UNPKG

phecda-server

Version:

server framework that provide IOC/type-reuse/http&rpc-adaptor

616 lines (588 loc) 16.9 kB
import { ERROR_SYMBOL, HMR, IS_DEV, IS_PURE, IS_STRICT, LOG_LEVEL, __name, log } from "./chunk-NQ55PA2X.mjs"; // src/helper.ts import pc2 from "picocolors"; // src/context.ts import Debug from "debug"; import pc from "picocolors"; // src/pipe.ts import { isPhecda, validate } from "phecda-core"; // src/exception/base.ts var Exception = class extends Error { static { __name(this, "Exception"); } message; status; description; constructor(message, status = 0, description = "Exception") { super(message), this.message = message, this.status = status, this.description = description; } get data() { return { message: this.message, description: this.description, status: this.status, [ERROR_SYMBOL]: true }; } }; // src/exception/undefine.ts var UndefinedException = class extends Exception { static { __name(this, "UndefinedException"); } constructor(message) { super(message, 500, "Undefined error"); } }; // src/exception/validate.ts var ValidateException = class extends Exception { static { __name(this, "ValidateException"); } constructor(message) { super(message, 400, "Validate exception"); } }; // src/exception/forbidden.ts var ForbiddenException = class extends Exception { static { __name(this, "ForbiddenException"); } constructor(message) { super(message, 403, "Forbidden resource"); } }; // src/exception/bad-request.ts var BadRequestException = class extends Exception { static { __name(this, "BadRequestException"); } constructor(message) { super(message, 400, "Bad Request"); } }; // src/exception/not-found.ts var NotFoundException = class extends Exception { static { __name(this, "NotFoundException"); } constructor(message) { super(message, 404, "Not Found"); } }; // src/exception/conflict.ts var ConflictException = class extends Exception { static { __name(this, "ConflictException"); } constructor(message) { super(message, 409, "Conflict"); } }; // src/exception/bad-gateway.ts var BadGatewayException = class extends Exception { static { __name(this, "BadGatewayException"); } constructor(message) { super(message, 502, "Bad Gatrway"); } }; // src/exception/invalid-input.ts var InvalidInputException = class extends Exception { static { __name(this, "InvalidInputException"); } constructor(message) { super(message, 502, "Invalid Input"); } }; // src/exception/media-type.ts var UnsupportedMediaTypeException = class extends Exception { static { __name(this, "UnsupportedMediaTypeException"); } constructor(message) { super(message, 415, "Unsupported Media Type"); } }; // src/exception/payload-large.ts var PayloadLargeException = class extends Exception { static { __name(this, "PayloadLargeException"); } constructor(message) { super(message, 413, "Payload Too Large"); } }; // src/exception/timeout.ts var TimeoutException = class extends Exception { static { __name(this, "TimeoutException"); } constructor(message) { super(message, 408, "Request Timeout"); } }; // src/exception/unauthorized.ts var UnauthorizedException = class extends Exception { static { __name(this, "UnauthorizedException"); } constructor(message) { super(message, 401, "Unauthorized"); } }; // src/exception/unavailable-service.ts var ServiceUnavailableException = class extends Exception { static { __name(this, "ServiceUnavailableException"); } constructor(message) { super(message, 503, "Service Unavailable"); } }; // src/exception/framework.ts var FrameworkException = class extends Exception { static { __name(this, "FrameworkException"); } constructor(message) { super(`[phecda-server] ${message}`, 500, "Framework Error"); } }; // src/exception/timer.ts var TimerException = class extends Exception { static { __name(this, "TimerException"); } constructor(message) { super(message, 0, "Timer Error"); } }; // src/exception/worker.ts var WorkerException = class extends Exception { static { __name(this, "WorkerException"); } constructor(message) { super(message, 0, "Worker Error"); } }; // src/pipe.ts var defaultPipe = /* @__PURE__ */ __name(async ({ arg, reflect, meta, index, type }, { method }) => { if (meta.const) { if (arg !== meta.const) throw new ValidateException(`param ${index + 1} must be ${meta.const}`); } if (arg === void 0) { if (meta.required === false) return arg; else throw new ValidateException(`param ${index + 1} is required`); } if ([ "params", "query" ].includes(type)) { if (reflect === Number) { arg = reflect(arg); if (isNaN(arg)) throw new ValidateException(`param ${index + 1} is not a number`); } else if (reflect === Boolean) { if (arg === "false") arg = false; else if (arg === "true") arg = true; else throw new ValidateException(`param ${index + 1} is not a boolean`); } } else { if (reflect === Number && typeof arg !== "number") throw new ValidateException(`param ${index + 1} is not a number`); if (reflect === Boolean && typeof arg !== "boolean") throw new ValidateException(`param ${index + 1} is not a boolean`); if (reflect === String && typeof arg !== "string") throw new ValidateException(`param ${index + 1} is not a string`); } if (meta.enum) { if (!Object.values(meta.enum).includes(arg)) throw new ValidateException(`param ${index + 1} is not a valid enum value`); } if (meta.oneOf) { let isCorrect = false; for (const item of meta.oneOf) { switch (item) { case String: if (typeof arg === "string") isCorrect = true; break; case Number: if (typeof arg === "number") isCorrect = true; break; case Boolean: if (typeof arg === "boolean") isCorrect = true; break; default: if (isPhecda(item)) { const errs = await validate(item, arg); if (!errs.length) { isCorrect = true; break; } } else if (typeof item === "function") { const ret = await item(arg); if (ret) { isCorrect = true; break; } } else { if (arg === item) { isCorrect = true; break; } } } } if (!isCorrect) throw new ValidateException(`param ${index + 1} can't pass one of these validations`); } if (meta.rules) { for (const rule of meta.rules) { const err = await rule({ value: arg, property: method, meta, model: reflect, index }); if (err.length > 0) throw new ValidateException(err[0]); } } if (isPhecda(reflect)) { const errs = await validate(reflect, arg); if (errs.length) throw new ValidateException(errs[0]); } return arg; }, "defaultPipe"); // src/filter.ts var defaultFilter = /* @__PURE__ */ __name((e) => { if (!(e instanceof Exception)) { log(e.message, "error"); if (LOG_LEVEL <= 0) console.error(e.stack); e = new UndefinedException(e.message || e); } else { log(`[${e.constructor.name}] ${e.message}`, "error"); if (LOG_LEVEL <= 0) console.error(e.stack); } return e.data; }, "defaultFilter"); // src/context.ts var debug = Debug("phecda-server(Context)"); var Context = class _Context { static { __name(this, "Context"); } data; method; params; static filterRecord = { default: defaultFilter }; static pipeRecord = { default: defaultPipe }; static guardRecord = {}; static addonRecord = {}; ctx; // protected canGetCtx = true constructor(data) { this.data = data; if (IS_DEV) data._context = this; this.ctx = new Proxy(data, { get(target, p) { if (!(p in target)) log(`attribute "${p}" does not exist on ctx, which might be due to a missing AOP role (such as a guard).`, "warn", data.tag); return target[p]; }, set(target, p, newValue) { target[p] = newValue; return true; } }); } static getAop(meta, opts) { const { globalGuards = [], globalFilter = "default", globalPipe = "default" } = opts; const { data: { guards, filter, params, tag, method } } = meta; const resolved = { guards: [ ...globalGuards, ...guards ], pipe: params.map((item) => item.pipe || globalPipe), filter: filter || globalFilter }; if (process.env.DEBUG) { const { guards: guards2, pipe, filter: filter2 } = resolved; debug(`method "${tag}-${method}" aop: ${pc.magenta(`Guard ${guards2.join("->")}[${guards2.filter((g) => g in this.guardRecord).join("->")}]`)} ${pc.blue(`Pipe ${pipe.join("-")}[${pipe.map((p) => p in this.pipeRecord ? p : "default").join("-")}]`)} ${pc.red(`Filter ${filter2}[${filter2 || "default"}]`)}`); } return { guards: this.getGuards(resolved.guards), pipe: this.getPipe(resolved.pipe), filter: this.getFilter(resolved.filter) }; } async run({ guards, filter, pipe }, successCb, failCb) { const { meta, moduleMap } = this.data; const { paramsType, data: { ctxs, tag, params, method } } = meta; try { let res; const nextHandler = /* @__PURE__ */ __name((index) => { return async () => { if (index === guards.length) { const instance = moduleMap.get(tag); if (ctxs) { ctxs.forEach((ctx) => instance[ctx] = this.ctx); } const args = await Promise.all(params.map((item, i) => pipe[i]({ arg: resolveDep(this.data[item.type], item.key), reflect: paramsType[item.index], ...item }, this.ctx))); res = await instance[method](...args); } else { let nextPromise; async function next() { return nextPromise = nextHandler(index + 1)().then((ret2) => { if (ret2 !== void 0) { debug(`The ${index + 1}th guard on "${tag}-${method}" rewrite the response value.`); res = ret2; } return res; }); } __name(next, "next"); const ret = await guards[index](this.ctx, next); if (ret !== void 0) { res = ret; } else { if (!nextPromise) await next(); else await nextPromise; } } }; }, "nextHandler"); await nextHandler(0)(); return successCb(res); } catch (e) { const err = await filter(e, this.ctx); return failCb(err); } } static getPipe(pipe) { return pipe.map((pipe2) => { return _Context.pipeRecord[pipe2] || _Context.pipeRecord.default; }); } static getFilter(filter = "default") { return _Context.filterRecord[filter] || _Context.filterRecord.default; } static getGuards(guards) { const ret = []; for (const guard of new Set(guards)) { if (guard in _Context.guardRecord) ret.push(_Context.guardRecord[guard]); } return ret.sort((a, b) => b.priority - a.priority).map((item) => item.value); } static applyAddons(addons, router, framework) { const ret = []; for (const a of new Set(addons)) { if (a in _Context.addonRecord) ret.push(_Context.addonRecord[a]); } ret.sort((a, b) => b.priority - a.priority).forEach((item) => item.value(router, framework)); } }; function addPipe(key, pipe) { if (Context.pipeRecord[key] && Context.pipeRecord[key] !== pipe) debug(`overwrite Pipe "${String(key)}"`, "warn"); Context.pipeRecord[key] = pipe; } __name(addPipe, "addPipe"); function addFilter(key, filter) { if (Context.filterRecord[key] && Context.filterRecord[key] !== filter) debug(`overwrite Filter "${String(key)}"`, "warn"); Context.filterRecord[key] = filter; } __name(addFilter, "addFilter"); function addGuard(key, guard, priority = 0) { if (Context.guardRecord[key] && Context.guardRecord[key].value !== guard) debug(`overwrite Guard "${String(key)}"`, "warn"); Context.guardRecord[key] = { value: guard, priority }; } __name(addGuard, "addGuard"); function addAddon(key, addon, priority = 0) { if (Context.addonRecord[key] && Context.addonRecord[key].value !== addon) debug(`overwrite Addon "${String(key)}"`, "warn"); Context.addonRecord[key] = { value: addon, priority }; } __name(addAddon, "addAddon"); // src/http/helper.ts function resolveDep(ret, key) { if (key) return ret?.[key]; return ret; } __name(resolveDep, "resolveDep"); // src/decorators/helper.ts function shallowClone(obj) { return { ...obj }; } __name(shallowClone, "shallowClone"); function mergeObject(...args) { return Object.assign({}, ...args); } __name(mergeObject, "mergeObject"); // src/helper.ts function createControllerMetaMap(meta, filter) { const metaMap = /* @__PURE__ */ new Map(); function handleMeta() { metaMap.clear(); for (const item of meta) { const { tag, method } = item.data; if (!filter(item)) continue; if (metaMap.has(tag)) metaMap.get(tag)[method] = item; else metaMap.set(tag, { [method]: item }); } } __name(handleMeta, "handleMeta"); handleMeta(); HMR(handleMeta); return metaMap; } __name(createControllerMetaMap, "createControllerMetaMap"); function detectAopDep(meta, { guards, addons } = {}, controller = "http") { if (IS_PURE) return; const addonSet = /* @__PURE__ */ new Set(); const guardSet = /* @__PURE__ */ new Set(); const pipeSet = /* @__PURE__ */ new Set(); const filterSet = /* @__PURE__ */ new Set(); const warningSet = /* @__PURE__ */ new Set(); function handleMeta() { addonSet.clear(); guardSet.clear(); pipeSet.clear(); filterSet.clear(); warningSet.clear(); addons?.forEach((item) => { addonSet.add(item); }); guards?.forEach((item) => { guardSet.add(item); }); meta.forEach(({ data }) => { if (!data.controller) return; if (typeof data.tag !== "string") warningSet.add(`Tag of controller "${data.name}" should be a string`); if (data.controller !== controller) { if (data[controller]) warningSet.add(`Should use ${controller} controller to decorate class "${data.name}"`); return; } if (data.filter) filterSet.add(data.filter); data.guards.forEach((i) => guardSet.add(i)); data.addons.forEach((i) => addonSet.add(i)); data.params.forEach((i) => { if (i.pipe) pipeSet.add(i.pipe); }); }); const missAddons = [ ...addonSet ].filter((i) => !Context.addonRecord[i]); const missGuards = [ ...guardSet ].filter((i) => !Context.guardRecord[i]); const missPipes = [ ...pipeSet ].filter((i) => !Context.pipeRecord[i]); const missFilters = [ ...filterSet ].filter((i) => !Context.filterRecord[i]); function exit() { if (IS_STRICT) { log("Does not meet strict mode requirements", "error"); process.exit(1); } } __name(exit, "exit"); if (missAddons.length) { log(`${pc2.white(`Addon [${missAddons.join(",")}]`)} doesn't exist`, "warn"); exit(); } if (missGuards.length) { log(`${pc2.magenta(`Guard [${missGuards.join(",")}]`)} doesn't exist`, "warn"); exit(); } if (missPipes.length) { log(`${pc2.blue(`Pipe [${missPipes.join(",")}]`)} doesn't exist`, "warn"); exit(); } if (missFilters.length) { log(`${pc2.red(`Filter [${missFilters.join(",")}]`)} doesn't exist`, "warn"); exit(); } warningSet.forEach((warn) => log(warn, "warn")); if (warningSet.size) exit(); } __name(handleMeta, "handleMeta"); handleMeta(); HMR(handleMeta); return { addonSet, guardSet, pipeSet, filterSet }; } __name(detectAopDep, "detectAopDep"); function joinUrl(base, ...paths) { const joinedPath = [ base, ...paths ].filter((p) => p).map((path) => path.replace(/(^\/)/g, "")).join("/"); return `/${joinedPath}`; } __name(joinUrl, "joinUrl"); export { Exception, UndefinedException, ValidateException, ForbiddenException, BadRequestException, NotFoundException, ConflictException, BadGatewayException, InvalidInputException, UnsupportedMediaTypeException, PayloadLargeException, TimeoutException, UnauthorizedException, ServiceUnavailableException, FrameworkException, TimerException, WorkerException, defaultPipe, Context, addPipe, addFilter, addGuard, addAddon, resolveDep, shallowClone, mergeObject, createControllerMetaMap, detectAopDep, joinUrl };