UNPKG

@msom/http

Version:

@msom/http

858 lines (841 loc) 28.1 kB
import cors from "cors"; import express, { Router } from "express"; import http from "http"; import https from "https"; import net from "net"; import { Db as Db$1, MongoClient as MongoClient$1 } from "mongodb"; import { assert, createJsonRequestJson } from "@msom/common"; import bodyParser from "body-parser"; //#region src/http/http-proxy.ts /** * 创建代理中间件 * * @param {ProxyOptions} options - 代理配置选项 * @returns Express 中间件函数 */ function createProxyMiddleware(options) { return (req, res, next) => { if (options.bypass) { const bypassResult = options.bypass(req); if (bypassResult === false) return next(); } const targetUrl = new URL(options.target); let rewrittenPath = req.path; if (options.pathRewrite) if (typeof options.pathRewrite === "function") rewrittenPath = options.pathRewrite(req.path);else for (const [pattern, replacement] of Object.entries(options.pathRewrite)) { const regex = new RegExp(pattern); rewrittenPath = rewrittenPath.replace(regex, replacement); } const requestOptions = { hostname: targetUrl.hostname, port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80), path: rewrittenPath + (req.url.includes("?") ? `?${req.url.split("?")[1]}` : ""), method: req.method, headers: { ...req.headers } }; if (options.changeOrigin !== false) requestOptions.headers["host"] = targetUrl.host; const requestModule = targetUrl.protocol === "https:" ? https : http; const proxyReq = requestModule.request(requestOptions, (proxyRes) => { res.status(proxyRes.statusCode || 500); Object.entries(proxyRes.headers).forEach(([key, value]) => { if (value) res.setHeader(key, value); }); proxyRes.pipe(res); }); proxyReq.on("error", (err) => { if (options.onError) options.onError(err, req, res);else { console.error(`Proxy error: ${err.message}`); res.status(500).send("Proxy error"); } }); if (req.body && Object.keys(req.body).length > 0) proxyReq.write(JSON.stringify(req.body)); req.pipe(proxyReq); }; } /** * 创建 WebSocket 代理中间件 * * @param options - 代理配置选项 * @returns Express 中间件函数 */ function createWebSocketProxy(options) { return (req, res, next) => { if (req.headers.upgrade !== "websocket") return next(); const targetUrl = new URL(options.target); const proxySocket = net.connect(+targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80), targetUrl.hostname, () => { const headers = [ `GET ${req.url} HTTP/1.1`, `Host: ${options.changeOrigin !== false ? targetUrl.host : req.headers.host}`, `Connection: Upgrade`, `Upgrade: websocket`, `Sec-WebSocket-Version: ${req.headers["sec-websocket-version"]}`, `Sec-WebSocket-Key: ${req.headers["sec-websocket-key"]}`]; proxySocket.write(headers.join("\r\n") + "\r\n\r\n"); }); const clientSocket = req.socket; clientSocket.pipe(proxySocket); proxySocket.pipe(clientSocket); clientSocket.on("error", () => proxySocket.destroy()); proxySocket.on("error", () => clientSocket.destroy()); }; } /** * 创建代理服务器 * * @param app - Express 应用实例 * @param proxyRules - 代理规则配置 * @example // 示例使用 const app = express(); app.use(express.json()); // 配置代理规则 const proxyConfig: ProxyRules = { "/api": { target: "http://localhost:3001", changeOrigin: true, pathRewrite: { "^/api": "" }, ws: true, bypass: (req) => { // 绕过 POST 请求 return req.method === "POST" ? false : undefined; }, onError: (err, req, res) => { console.error(`API Proxy Error: ${err.message}`); res.status(502).json({ error: "Bad Gateway" }); }, }, "/external": { target: "https://jsonplaceholder.typicode.com", changeOrigin: true, pathRewrite: (path) => path.replace(/^\/external/, ""), secure: false, // 开发环境忽略 SSL 错误 }, "/images": "http://localhost:3002", // 简写形式 }; // 设置代理 setupProxy(app, proxyConfig); // 启动服务器 const PORT = 3000; app.listen(PORT, () => { console.log(`Proxy server running at http://localhost:${PORT}`); console.log("Available proxies:"); console.log(" /api -> http://localhost:3001"); console.log(" /external -> https://jsonplaceholder.typicode.com"); console.log(" /images -> http://localhost:3002"); }); * } */ function setupProxy(app, proxyRules) { Object.entries(proxyRules).forEach(([path, rule]) => { const options = typeof rule === "string" ? { target: rule } : rule; const proxyMiddleware = createProxyMiddleware({ ...options, target: options.target }); app.use(path, proxyMiddleware); if (options.ws) { const wsProxy = createWebSocketProxy(options); app.use(path, wsProxy); } }); } //#endregion //#region src/http/print-proxy.ts const NO_PROXY = "No proxy rules configured"; const Available_Proxies = "Available Proxies:"; /** * 生成详细代理规则报告(对齐格式) * @param proxyRules 代理规则配置 * @returns 格式化的代理规则描述数组 */ function generateAlignedProxyReport(proxyRules) { if (!proxyRules || Object.keys(proxyRules).length === 0) return [NO_PROXY]; const maxPathLength = Math.max(...Object.keys(proxyRules).map((path) => { const formattedPath = path.endsWith("/") ? path : `${path}/`; return formattedPath.length; })); return Object.entries(proxyRules).map(([path, rule]) => { const formattedPath = path.endsWith("/") ? path : `${path}/`; const target = typeof rule === "string" ? rule : rule.target; const baseLine = ` ${formattedPath.padEnd(maxPathLength)} -> ${target}`; if (typeof rule !== "string") { const options = []; if (rule.changeOrigin !== void 0) options.push(`changeOrigin: ${rule.changeOrigin}`); if (rule.secure !== void 0) options.push(`secure: ${rule.secure}`); if (rule.ws) options.push(`ws: true`); if (rule.pathRewrite) options.push(`pathRewrite: ${typeof rule.pathRewrite}`); if (rule.bypass) options.push(`bypass: function`); if (options.length > 0) { const optionIndent = " ".repeat(maxPathLength + 6); const optionLines = options.map((opt, i) => i === 0 ? `[${opt}` : `${optionIndent} ${opt}`); optionLines[optionLines.length - 1] += "]"; return `${baseLine} ${optionLines.join("\n" + optionIndent)}`; } } return baseLine; }); } /** * 打印对齐的代理服务器信息 * @param port 服务器端口 * @param proxyRules 代理规则配置 */ function printAlignedProxyServerInfo(port, proxyRules, printer = console.log.bind(console)) { if (proxyRules == void 0) return; printer(`Proxy server running at http://localhost:${port}`); const rules = generateAlignedProxyReport(proxyRules); if (rules[0] !== NO_PROXY) printer(Available_Proxies); rules.forEach((rule) => printer(rule)); } //#endregion //#region src/http/createServer.ts /** * * @param port * @param option * @returns * @example createServer(8088, { routes: [ { path: "/api", children: [ { path: "/userCreate", method: "post", handlers: [ express.json(), (request, response) => { console.log("aaa"); response.send({ aaa: 1 }); }, ], }, ], }, ], createHandle: () => { console.log("userServer ready"); }, }); */ function createServer(port, option = {}) { const server = express(); const { createHandle, routes, middles, proxy } = option; [ cors({ origin: "*" }), express.json(), middles || []]. flat().reduce((a, b) => a.use(b), server); if (routes) { const parseRoute = (routes$1, parentPath) => { for (const route of routes$1) { let { path, children, method, handlers = [] } = route; path = parentPath + path; if (method) { const requestF = server[method].bind(server); requestF(path, ...handlers); } if (children) parseRoute(children, path); } }; parseRoute(routes, ""); } if (proxy) setupProxy(server, proxy); server.listen(port, () => { typeof createHandle === "function" && createHandle({ port }); option.printProxy !== false && printAlignedProxyServerInfo(port, proxy); }); return server; } function staticMiddle(path) { return express.static(path); } //#endregion //#region src/DB/index.ts var MongoClient = class extends MongoClient$1 { db(dbName, options) { return new Db(this, dbName, options); } }; var Db = class extends Db$1 { get metaCollection() { return super.collection("__meta"); } collection(name, options, meta) { const _collection = super.collection(name, options); const metas = this.getCollectionMetaData(); metas.then((metas$1) => { if (meta && !metas$1.some((c) => c.collectionName === name)) this.setCollectionMetaData(meta); }); return _collection; } async createCollection(name, options, meta) { const _meta = await this.getCollectionMetaData(name); if (!_meta && meta) { if (!(await this.setCollectionMetaData({ ...meta, collectionName: name }))) throw new Error("set meta Error"); } return super.createCollection(name, options); } async getCollectionMetaData(collectionName) { if (!collectionName) return await this.metaCollection.find().toArray();else return await this.metaCollection.findOne({ collectionName: { $eq: collectionName } }); } async setCollectionMetaData(meta) { const res = await this.metaCollection.updateOne({ collectionName: meta.collectionName }, { $set: meta }, { upsert: true }); return !!(res.matchedCount + res.modifiedCount + res.upsertedCount); } async __update(sourceCollectionName, metaKey, getKey, news) { const meta = await this.getCollectionMetaData(sourceCollectionName); if (!meta) return this.setCollectionMetaData({ collectionName: sourceCollectionName, [metaKey]: news });else { const newIndexs = this.check(news, getKey); const length = meta[metaKey].length; for (let i = 0; i < length; i++) { const k = getKey(meta[metaKey][i], i); const newIndex = newIndexs.get(k); if (newIndex != void 0) { meta[metaKey].splice(i, 1, news[newIndex]); newIndexs.delete(k); } } newIndexs.forEach((newIndex) => { meta[metaKey].push(news[newIndex]); }); return this.setCollectionMetaData(meta); } } updateRelates(sourceCollectionName, ...relates) { return this.__update(sourceCollectionName, "relates", ({ relateName }) => relateName, relates); } updateProps(sourceCollectionName, ...props) { return this.__update(sourceCollectionName, "props", ({ prop }) => prop.name, props); } updateExtends(sourceCollectionName, ..._extends) { return this.__update(sourceCollectionName, "extends", ({ collectionName }) => collectionName, _extends); } fillMate(meta) { return { props: [], extends: [], relates: [], collectionName: meta.collectionName }; } check(props, getKey) { const propIndexs = /* @__PURE__ */new Map(); for (let index = 0; index < props.length; index++) { const key = getKey(props[index], index); if (propIndexs.has(key)) throw Error("has repeat propName!"); propIndexs.set(key, index); } return propIndexs; } }; //#endregion //#region src/result/CodeResult.ts var CodeResultConstructor = class { constructor(code, message, payload) { this.code = code; if (payload) { if (typeof message === "object" && message !== null) throw Error(); if (message != void 0) this.message = message; this.payload = payload; } else if (message != void 0) if (typeof message === "object") this.payload = message;else this.message = message; } }; //#endregion //#region src/mongo-proxy/interfaces.ts var RelationType; (function (RelationType$1) { RelationType$1["ONE"] = "One"; RelationType$1["MANY"] = "Many"; })(RelationType || (RelationType = {})); var ConditionType; (function (ConditionType$1) { ConditionType$1["COMP"] = "comp"; ConditionType$1["AND"] = "and"; ConditionType$1["OR"] = "or"; })(ConditionType || (ConditionType = {})); var CompCondition = class CompCondition { static is(condition) { return condition.type === ConditionType.COMP; } static CompHandle = { "!=": (modelValue, targetValue) => modelValue !== targetValue, "=": (modelValue, targetValue) => modelValue === targetValue, ">": (modelValue, targetValue) => modelValue > targetValue, "<": (modelValue, targetValue) => modelValue < targetValue, "<=": (modelValue, targetValue) => modelValue <= targetValue, ">=": (modelValue, targetValue) => modelValue >= targetValue, in: (modelValue, targetValue) => targetValue.includes(modelValue), nin: (modelValue, targetValue) => !targetValue.includes(modelValue), exist: (model, propKey) => Reflect.has(model, propKey) }; constructor(compType, propKey, value) { this.type = ConditionType.COMP; this.compType = compType; this.propKey = propKey; this.value = value; } clone() { return new CompCondition(this.compType, this.propKey, this.value); } filter(model) { const { compType, propKey, value } = this; const h = CompCondition.CompHandle[compType]; assert(h, "未知比较运算符"); if (compType === "exist") return h(model, propKey); return h(model[propKey], value); } }; var AndCondition = class AndCondition { static is(condition) { return condition.type === ConditionType.AND; } constructor(conditions) { this.type = ConditionType.AND; this.conditions = conditions; } clone() { return new AndCondition(this.conditions.map((v) => v.clone())); } filter(model) { return this.conditions.every((condition) => condition.filter(model)); } }; var OrCondition = class OrCondition { static is(condition) { return condition.type === ConditionType.OR; } constructor(conditions) { this.type = ConditionType.OR; this.conditions = conditions; } clone() { return new OrCondition(this.conditions.map((v) => v.clone())); } filter(model) { return this.conditions.some((condition) => condition.filter(model)); } }; //#endregion //#region src/mongo-proxy/QueryProtocolBuilder.ts var ModelType; (function (ModelType$1) { ModelType$1["RELATE"] = "relate"; ModelType$1["MODEL"] = "model"; })(ModelType || (ModelType = {})); var Model = class { type; conditions; relates; children; condition(condition) { this.conditions = condition; return this; } relate(relateName, callback) { const model = new RelateModel(relateName); callback?.(model); return this; } }; var QueryModel = class extends Model { constructor(modelName) { super(); this.type = ModelType.MODEL; this.modelName = modelName; } static is(model) { return model.type === ModelType.MODEL; } }; var RelateModel = class RelateModel extends Model { constructor(relateName) { super(); this.type = ModelType.RELATE; this.relateName = relateName; } recursive(count) { let that = this; for (let i = 1; i < count; i++) { const model = new RelateModel(this.relateName); model.conditions = that.conditions?.clone(); that.relates[this.relateName] = model; that = model; } return this; } static is(model) { return model.type === ModelType.RELATE; } }; var QueryProtocolBuilder = class { startModel; rootModel; currentModel; constructor(startModel) { this.startModel = startModel; this.currentModel = this.rootModel = new QueryModel(startModel); } model(modelName, callback) { if (!modelName) callback?.(this.rootModel); return this; } relate(relateName, callback) { const model = new RelateModel(relateName); const relate = this.relate.bind(this); model.recursive = function (count) { for (let i = 1; i < count; i++) relate(relateName); return this; }; this.currentModel.children = model; this.currentModel = model; callback?.(model); return this; } protocol() { return { start: this.startModel, option: this.rootModel }; } }; function comp(compType, propKey, value) { return new CompCondition(compType, propKey, value); } function and(...conditions) { return new AndCondition(conditions); } function or(...conditions) { return new OrCondition(conditions); } //#endregion //#region src/mongo-proxy/Client.ts var Client = class { constructor(option = {}) { const { host = "localhost", protocol = "http", api = "" } = option; let port = option.port; if (typeof port !== "number" || Math.min(Math.max(0, port), 65535) !== port) port = 5174; Object.assign(this, { host, port, protocol, api }); } get requestHref() { const { protocol, host, port, api } = this; return `${protocol}://${host}:${port}${api}`; } createQuery(protocol) { return createJsonRequestJson(this.requestHref + "/query", { body: JSON.stringify({ protocol }) }); } }; //#endregion //#region src/mongo-proxy/DBContext.ts const MODEL_METADATA_COLLECTION = "__model_metas__"; var DBContext = class { constructor(uri, option = {}) { this.client = new MongoClient$1(uri, { minPoolSize: 5, maxPoolSize: 50, connectTimeoutMS: 5e3, serverSelectionTimeoutMS: 5e3, ...option }); this.modelMetas = /* @__PURE__ */new Map(); this.isConnected = false; } async connect(dbName) { const { client } = this; try { await client.connect(); this.db = client.db(dbName); await this.loadModelMetas(); this.isConnected = true; console.log(`✅ Connected to MongoDB database: ${dbName}`); } catch (error) { this.isConnected = false; console.error("❌ MongoDB connection error:", error); throw error; } } async disconnect() { if (this.client) { await this.client.close(true); this.isConnected = false; console.log("🔌 Disconnected from MongoDB"); } } async checkConnection() { try { if (!this.db) return false; await this.db.command({ ping: 1 }); return true; } catch (error) { return false; } } async loadModelMetas() { if (!this.db) throw new Error("Database not connected"); try { const collection = this.db.collection(MODEL_METADATA_COLLECTION); const metas = await collection.find().toArray(); this.modelMetas.clear(); for (const meta of metas) this.modelMetas.set(meta.modelName, meta); console.log(`📚 Loaded ${metas.length} model metadata definitions`); } catch (error) { console.error("Failed to load model metadata:", error); } } async saveModelMeta(meta) { if (!this.db) throw new Error("Database not connected"); try { const collection = this.db.collection(MODEL_METADATA_COLLECTION); await collection.updateOne({ modelName: meta.modelName }, { $set: meta }, { upsert: true }); this.modelMetas.set(meta.modelName, meta); console.log(`💾 Saved model metadata for: ${meta.modelName}`); } catch (error) { console.error(`Failed to save model metadata for ${meta.modelName}:`, error); throw error; } } getModelMeta(modelName) { return this.modelMetas.get(modelName); } getAllModelNames() { return Array.from(this.modelMetas.keys()); } getCollection(modelName) { if (!this.db) throw new Error("Database not connected"); return this.db.collection(modelName); } }; //#endregion //#region src/mongo-proxy/QueryExecutor.ts const FilterTypeMap = { "!=": "$ne", "=": "$eq", "<": "$lt", "<=": "$lte", ">": "$gt", ">=": "$gte", in: "$in", nin: "$nin", exist: "$exist" }; var QueryExecutor = class { cache = /* @__PURE__ */new Map(); constructor(option) { Object.assign(option); } async clearCache() { this.cache.clear(); } async execute(protocol) { const cacheKey = this.generateCacheKey(protocol); const cache = this.cache.get(cacheKey); if (cache) return cache; const result = this.processProtocol(protocol); this.cache.set(cacheKey, result); return result; } resolveCondition(condition) { if (!condition) return {}; if (CompCondition.is(condition)) return { [condition.propKey]: { [FilterTypeMap[condition.compType]]: condition.compType === "exist" ? true : condition.value } }; return { [AndCondition.is(condition) ? "$and" : "$or"]: condition.conditions.map(this.resolveCondition) }; } async processProtocol(protocol) { const process$1 = async (option) => { const { modelName, conditions: condition } = option; const collection = this.dbContext.getCollection(modelName); const models = (await collection.find(this.resolveCondition(condition)).toArray()).map((_model) => { const { _id, ...model } = _model; return { _id, model, relates: {} }; }); /** * TODO * 判断查询条件有没有关系,即relates是否为空对象 * 否,则直接返回models * 有关系查询,则找到对应模型的meta元数据,处理meta.relations中存在的搞关系 * 在meta.relating中找到对应关系,根据实例id去找到关联的实例id * 并生成比对id的condition和查询条件中对应的RelateModel的condition合并 * 生成新的QueryModel,包含对应RelateModel的relates和meta中对应关系的modelName和合并后的condition * 递归处理新的QueryModel,将返回结果添加在当前model实例的relates中 */ const relateKeys = Reflect.ownKeys(option.relates); if (relateKeys.length === 0) return models; const relateHandles = models.map(async ({ _id, relates }) => { const handles = relateKeys.map((relationName) => { const meta = this.dbContext.getModelMeta(modelName); assert(meta, "unknown modelName: " + modelName); if (!Reflect.has(meta.relations, relationName) || !Reflect.has(meta.relating, relationName)) return; const relationMeta = Reflect.get(meta.relations, relationName, meta.relations); if (!relationMeta) return; const relating = meta.relating[relationName].get(_id); if (!relating) return; const relateModel = option.relates[relationName]; const { correspondingModel } = relationMeta; const newOption = new QueryModel(correspondingModel); newOption.relates = relateModel.relates; newOption.conditions = and(...[relateModel.conditions, comp("in", "_id", relating)].filter(Boolean)); return process$1(newOption).then((relations) => { relates[relationName] = relations; }); }).filter(Boolean); return Promise.all(handles); }); await Promise.all(relateHandles); return models; }; return await process$1(protocol.option); } generateCacheKey(protocol) { return JSON.stringify(protocol); } }; //#endregion //#region src/mongo-proxy/DatabaseProxyService.ts var DatabaseProxyService = class { dbContext; constructor(dbContext, option) { this.dbContext = dbContext; this.app = express(); this.queryExecutor = new QueryExecutor({ dbContext }); this.router = Router(); this.setupMiddleware(); this.setupRoutes(); this.setupApp(); this.base = ""; Object.assign(this, option || {}); } setupMiddleware() { this.app.use(bodyParser.json()); this.app.use((req, res, next) => { console.log(`${(/* @__PURE__ */new Date()).toLocaleTimeString()} - ${req.method} ${req.path}`); next(); }); } setupApp() { this.router.route(this.base); this.app.use(this.router); } setupRoutes() { this.router; this.router.post("/query", async (req, res) => { try { const { protocol } = req.body; if (!protocol || !protocol.start) return this.sendError(res, 400, "Invalid query protocol"); const result = await this.queryExecutor.execute(protocol); this.sendSuccess(res, result); } catch (error) { this.sendError(res, 500, error.message || "Query execution failed"); } }); this.router.post("/model-meta", async (req, res) => { try { const meta = req.body; if (!meta || !meta.modelName) return this.sendError(res, 400, "Invalid model metadata"); await this.dbContext.saveModelMeta(meta); this.sendSuccess(res, { success: true }); } catch (error) { this.sendError(res, 500, error.message || "Failed to save model metadata"); } }); this.router.get("/model-meta/:modelName", async (req, res) => { try { const modelName = req.params.modelName; const meta = this.dbContext.getModelMeta(modelName); if (!meta) return this.sendError(res, 404, `Model metadata not found for: ${modelName}`); this.sendSuccess(res, meta); } catch (error) { this.sendError(res, 500, error.message || "Failed to get model metadata"); } }); this.router.get("/models", async (req, res) => { try { const modelNames = this.dbContext.getAllModelNames(); this.sendSuccess(res, modelNames); } catch (error) { this.sendError(res, 500, error.message || "Failed to get model names"); } }); this.router.get("/health", async (req, res) => { try { const dbStatus = (await this.dbContext.checkConnection()) ? "connected" : "disconnected"; const response = { status: "ok", timestamp: (/* @__PURE__ */new Date()).toISOString(), uptime: process.uptime(), dbStatus }; res.json(response); } catch (error) { const response = { status: "error", timestamp: (/* @__PURE__ */new Date()).toISOString(), uptime: process.uptime(), dbStatus: "error" }; res.status(500).json(response); } }); this.router.post("/clear-cache", async (req, res) => { try { this.queryExecutor.clearCache(); this.sendSuccess(res, { message: "Cache cleared successfully" }); } catch (error) { this.sendError(res, 500, error.message || "Failed to clear cache"); } }); } sendSuccess(res, data, message) { res.json(createSuccessResponse(data, message)); } sendError(res, status, message, details) { res.status(status).json(createErrorResponse(message, details)); } start(port = 3e3) { this.app.listen(port, () => { console.log(`\n🚀 Database proxy service running on port ${port}`); console.log(`📊 Query endpoint: POST http://localhost:${port}/query`); console.log(`📝 Model meta endpoint: POST http://localhost:${port}/model-meta`); console.log(`📋 Model meta endpoint: GET http://localhost:${port}/model-meta/:modelName`); console.log(`📚 Models endpoint: GET http://localhost:${port}/models`); console.log(`🧹 Cache endpoint: POST http://localhost:${port}/clear-cache`); console.log(`❤️ Health check: GET http://localhost:${port}/health\n`); }); } }; function createSuccessResponse(data, message) { return { code: 0, success: true, data, message }; } function createErrorResponse(error, details) { return { code: 1, error, details }; } //#endregion export { AndCondition, Client, CodeResultConstructor as CodeResult, CompCondition, ConditionType, DBContext, DatabaseProxyService, Db, Model, ModelType, MongoClient, OrCondition, QueryExecutor, QueryModel, QueryProtocolBuilder, RelateModel, RelationType, and, comp, createErrorResponse, createProxyMiddleware, createServer, createSuccessResponse, createWebSocketProxy, generateAlignedProxyReport, or, printAlignedProxyServerInfo, setupProxy, staticMiddle }; //# sourceMappingURL=index.js.map