phecda-server
Version:
server framework that provide IOC/type-reuse/http&rpc-adaptor
616 lines (588 loc) • 16.9 kB
JavaScript
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
};