@aomex/web
Advanced tools
Comparing version 0.0.29 to 1.0.0
# @aomex/web | ||
## 0.0.29 | ||
## 1.0.0 | ||
### Patch Changes | ||
### Minor Changes | ||
- [`59cd3f9`](https://github.com/aomex/aomex/commit/59cd3f98a0b5648a972d6118ba21501130a9ee7e) Thanks [@geekact](https://github.com/geekact)! - chore(web): 升级插件版本 | ||
- [`11103a2`](https://github.com/aomex/aomex/commit/11103a2aff4e081754c56b0ff18fa5130ca252e8) Thanks [@geekact](https://github.com/geekact)! - 重新制作 | ||
- Updated dependencies [[`90ca7d5`](https://github.com/aomex/aomex/commit/90ca7d5fc736b523c4fbf7949f64653428dc413c)]: | ||
- @aomex/core@0.0.28 | ||
## 0.0.28 | ||
### Patch Changes | ||
- [`00bafbb`](https://github.com/aomex/aomex/commit/00bafbbac2d32205b63a6bf561fb0a69c38a54bb) Thanks [@geekact](https://github.com/geekact)! - refactor(core): 缓存迁移到新的包@aomex/internal-cache | ||
- [`8becf8e`](https://github.com/aomex/aomex/commit/8becf8ee5ef86a5909783d0654e536db7be9bf5b) Thanks [@geekact](https://github.com/geekact)! - refactor(core): 工具移动到新的包@aomex/internal-tools | ||
- Updated dependencies [[`00bafbb`](https://github.com/aomex/aomex/commit/00bafbbac2d32205b63a6bf561fb0a69c38a54bb), [`8becf8e`](https://github.com/aomex/aomex/commit/8becf8ee5ef86a5909783d0654e536db7be9bf5b), [`0776719`](https://github.com/aomex/aomex/commit/077671963401f1dafb5b03722899452d45df13fc), [`f996bf7`](https://github.com/aomex/aomex/commit/f996bf7e529e7751a5e858c579feed33f5f02d65)]: | ||
- @aomex/core@0.0.27 | ||
- @aomex/internal-tools@0.0.27 | ||
## 0.0.27 | ||
### Patch Changes | ||
- [`33c3c61`](https://github.com/aomex/aomex/commit/33c3c614be9f66077f7487350b88d2db854f5fc8) Thanks [@geekact](https://github.com/geekact)! - revert(web): 导出HttpError | ||
- Updated dependencies [[`fbcea3d`](https://github.com/aomex/aomex/commit/fbcea3d68ff033e6861130c645c8e5ad7336193f)]: | ||
- @aomex/core@0.0.26 | ||
## 0.0.26 | ||
### Patch Changes | ||
- [`5ef2022`](https://github.com/aomex/aomex/commit/5ef202248b4320ff9076fbecb54379055cffb7db) Thanks [@geekact](https://github.com/geekact)! - feat(web): export createHttpError function | ||
- [`8d9e9d8`](https://github.com/aomex/aomex/commit/8d9e9d8ece0bbcc7e1ac685702eacbfdfa145aa9) Thanks [@geekact](https://github.com/geekact)! - fix(web): auto convert to single file when files.length===1 | ||
- Updated dependencies [[`0e6ed2c`](https://github.com/aomex/aomex/commit/0e6ed2c611100dcfaafb6fb41357624ad9f5c67a)]: | ||
- @aomex/core@0.0.25 | ||
## 0.0.25 | ||
### Patch Changes | ||
- Updated dependencies [[`2ac62fd`](https://github.com/aomex/aomex/commit/2ac62fd28166a1d9dd60b3c6d5a6508a6f9ee82b), [`4258410`](https://github.com/aomex/aomex/commit/42584107ad9f7e34492ae1053fef83aa2d9d747a), [`4177cba`](https://github.com/aomex/aomex/commit/4177cba7877e38120842bd8d287eaed54e4926ca)]: | ||
- @aomex/core@0.0.24 | ||
## 0.0.24 | ||
### Patch Changes | ||
- [`6621e4f`](https://github.com/aomex/aomex/commit/6621e4ff0f3509beeb332a0571a7db9c7d6ca99a) Thanks [@geekact](https://github.com/geekact)! - fix(web): 文件验证器的hash类型错误 | ||
- [`46c5b72`](https://github.com/aomex/aomex/commit/46c5b72785011fa181767f4c8ea0d0f5008b21ae) Thanks [@geekact](https://github.com/geekact)! - feat(web): 增加aomex-ts-node执行文件 | ||
- Updated dependencies [[`7b09277`](https://github.com/aomex/aomex/commit/7b09277136910966f500c8132303c7ddee84340c), [`9c78999`](https://github.com/aomex/aomex/commit/9c78999ebcb2962f30344acfbf6de0733d6fdd41), [`f4b012d`](https://github.com/aomex/aomex/commit/f4b012d98cddb2918479ea05df6c266dd914e53a)]: | ||
- @aomex/core@0.0.23 | ||
## 0.0.23 | ||
### Patch Changes | ||
- [`e21007a`](https://github.com/aomex/aomex/commit/e21007a82cb8eac73e1f696340bbe986d57dc159) Thanks [@geekact](https://github.com/geekact)! - chore(web): 升级依赖formidable到v3.4.0 | ||
- [`e21007a`](https://github.com/aomex/aomex/commit/e21007a82cb8eac73e1f696340bbe986d57dc159) Thanks [@geekact](https://github.com/geekact)! - feat(web): 验证响应的数据 | ||
- [`818e840`](https://github.com/aomex/aomex/commit/818e840d36c7456a863fc071968b246c123c17f5) Thanks [@geekact](https://github.com/geekact)! - refactor(core): 对象使用统一逻辑转换成验证器 | ||
- [`fb6c72d`](https://github.com/aomex/aomex/commit/fb6c72dbb266be4db92a542afe93dfa5d8c7cd41) Thanks [@geekact](https://github.com/geekact)! - chore(web): 升级依赖 formidable 3.4.0 -> 3.5.0 | ||
chore(web): 升级依赖 qs 6.11.1 -> 6.11.2 | ||
- Updated dependencies [[`6f7d706`](https://github.com/aomex/aomex/commit/6f7d7066c23711abdd149eb1c9a293ab8c4284a4), [`e7bf93c`](https://github.com/aomex/aomex/commit/e7bf93cee6896c61d0bf3eb0921151dc6c1bc107), [`818e840`](https://github.com/aomex/aomex/commit/818e840d36c7456a863fc071968b246c123c17f5)]: | ||
- @aomex/core@0.0.22 | ||
- Updated dependencies [[`11103a2`](https://github.com/aomex/aomex/commit/11103a2aff4e081754c56b0ff18fa5130ca252e8)]: | ||
- @aomex/core@1.0.0 | ||
- @aomex/internal-tools@1.0.0 |
@@ -1,107 +0,126 @@ | ||
import { Chain, PureChain, PureMiddlewareToken, Next, Middleware, OpenAPI, Validator, TransformedValidator, magistrate, CompatibleValidator } from '@aomex/core'; | ||
import { NonReadonly } from '@aomex/internal-tools'; | ||
import { Server, RequestListener, ServerResponse, IncomingMessage } from 'node:http'; | ||
import EventEmitter from 'node:events'; | ||
import { I18nFormat, Next, Middleware, OpenAPI, MiddlewareChain, MixinMiddlewareToken, Validator, TransformedValidator, magistrate, MixinMiddlewareChain, I18n, ValidatorToken } from '@aomex/core'; | ||
import { IncomingMessage, ServerResponse, OutgoingHttpHeaders, RequestListener, Server } from 'node:http'; | ||
import { Stream, EventEmitter } from 'node:stream'; | ||
import { Accepts } from 'accepts'; | ||
import { HttpError } from 'http-errors'; | ||
export { HttpError, default as createHttpError } from 'http-errors'; | ||
import { IParseOptions } from 'qs'; | ||
import { CookieParseOptions, CookieSerializeOptions } from 'cookie'; | ||
import { Accepts } from 'accepts'; | ||
import { Stream } from 'node:stream'; | ||
import contentDisposition from 'content-disposition'; | ||
import { CookieSerializeOptions } from 'cookie'; | ||
import { NonReadonly } from '@aomex/internal-tools'; | ||
import { File } from 'formidable'; | ||
import { RequestListener as RequestListener$1, Server as Server$1 } from 'http'; | ||
export { default as statuses } from 'statuses'; | ||
declare module '@aomex/core' { | ||
interface ChainPlatform { | ||
readonly web: WebChain; | ||
namespace I18n { | ||
interface Definition { | ||
web: { | ||
validator: { | ||
file: { | ||
must_be_file: I18nFormat<{ | ||
label: string; | ||
}>; | ||
too_large: I18nFormat<{ | ||
label: string; | ||
}>; | ||
unsupported_mimetype: I18nFormat<{ | ||
label: string; | ||
}>; | ||
}; | ||
}; | ||
}; | ||
} | ||
} | ||
} | ||
type WebMiddlewareToken<P extends object = object> = WebMiddleware<P> | WebChain<P> | PureMiddlewareToken<P>; | ||
declare class WebChain<Props extends object = object> extends Chain<Props> { | ||
protected _web_chain_: 'web-chain'; | ||
mount: { | ||
<P extends object>(middleware: WebChain | PureChain | WebMiddlewareToken<P> | null): WebChain<Props & P>; | ||
}; | ||
} | ||
interface WebAppOption { | ||
type UpperHeaderKeys = UpperArrayHeaderKeys | UpperStringHeaderKeys; | ||
type UpperArrayHeaderKeys = 'Set-Cookie'; | ||
type UpperStringHeaderKeys = UpperExternalStringHeaderKeys | UpperOfficialStringHeaderKeys; | ||
type LowerExternalStringHeaderKeys = 'accept-encoding' | 'x-forwarded-for' | 'x-forwarded-proto' | 'x-forwarded-host' | 'x-real-ip' | 'access-control-request-private-network' | 'access-control-allow-private-network' | ':authority'; | ||
type UpperExternalStringHeaderKeys = 'Accept-Encoding' | 'X-Forwarded-For' | 'X-Forwarded-Proto' | 'X-Forwarded-Host' | 'X-Real-IP' | 'Access-Control-Request-Private-Network' | 'Access-Control-Allow-Private-Network'; | ||
type UpperOfficialStringHeaderKeys = 'Accept' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Ranges' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Age' | 'Allow' | 'Alt-Svc' | 'Authorization' | 'Cache-Control' | 'Connection' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Type' | 'Cookie' | 'Date' | 'Etag' | 'Expect' | 'Expires' | 'Forwarded' | 'From' | 'Host' | 'If-Match' | 'If-Modified-Since' | 'If-None-Match' | 'If-Unmodified-Since' | 'Last-Modified' | 'Location' | 'Origin' | 'Pragma' | 'Proxy-Authenticate' | 'Proxy-Authorization' | 'Public-Key-Pins' | 'Range' | 'Referer' | 'Retry-After' | 'Sec-Websocket-Accept' | 'Sec-Websocket-Extensions' | 'Sec-Websocket-Key' | 'Sec-Websocket-Protocol' | 'Sec-Websocket-Version' | 'Strict-Transport-Security' | 'Tk' | 'Trailer' | 'Transfer-Encoding' | 'Upgrade' | 'User-Agent' | 'Vary' | 'Via' | 'Warning' | 'WWW-Authenticate'; | ||
declare class WebRequest { | ||
app: WebApp; | ||
req: IncomingMessage; | ||
res: ServerResponse; | ||
ctx: WebContext; | ||
url: string; | ||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'CONNECT' | (string & {}); | ||
params: Record<string, unknown>; | ||
private _accept?; | ||
private _cookies?; | ||
private _body?; | ||
private _parsedUrl; | ||
private _query?; | ||
get accept(): Accepts; | ||
get contentType(): string; | ||
/** | ||
* 静默模式。默认值:`false` | ||
* 返回从`headers['cookie']`解析后的cookie列表 | ||
*/ | ||
silent?: boolean; | ||
get cookies(): { | ||
readonly [key: string]: string | undefined; | ||
}; | ||
get body(): Promise<unknown>; | ||
get fresh(): boolean; | ||
get host(): string; | ||
/** | ||
* 调试模式。默认值:`process.env.NODE_ENV !== 'production'` | ||
* 包括了协议,域名,端口和路径的完整链接 | ||
*/ | ||
debug?: boolean; | ||
get href(): string; | ||
get ip(): string; | ||
get origin(): string; | ||
get pathname(): string; | ||
get protocol(): string; | ||
/** | ||
* 查询字符串的解析配置。使用依赖库`qs`作为解析器 | ||
* @see WebRequest.query | ||
* 查询字符串对象 | ||
*/ | ||
query?: IParseOptions; | ||
cookie?: { | ||
/** | ||
* `ctx.request.cookie`的配置 | ||
* @see WebRequest.cookie | ||
*/ | ||
get?: CookieParseOptions; | ||
/** | ||
* `ctx.response.cookie`的配置 | ||
* @see WebResponse.cookie | ||
*/ | ||
set?: CookieSerializeOptions; | ||
}; | ||
get query(): Record<string, unknown>; | ||
/** | ||
* 是否验证响应数据。如果不设置,则在debug开启的情况下进行验证操作 | ||
* @see response() | ||
* 查询字符串 | ||
*/ | ||
validateResponse?: boolean; | ||
get querystring(): string; | ||
/** | ||
* 搜索字符串,比查询字符串多了一个开头问号(?) | ||
*/ | ||
get search(): string; | ||
get secure(): boolean; | ||
protected get URL(): URL; | ||
matchContentType(type: string, ...types: string[]): string | null; | ||
} | ||
declare class WebApp extends EventEmitter { | ||
readonly options: WebAppOption; | ||
readonly chainPoints: string[]; | ||
protected readonly middlewareList: WebMiddlewareToken[]; | ||
constructor(options?: WebAppOption); | ||
get debug(): boolean; | ||
listen: Server['listen']; | ||
callback(): RequestListener<any, any>; | ||
log(err: HttpError): void; | ||
on(eventName: 'error', listener: (err: HttpError, ctx: WebContext) => void): this; | ||
on(eventName: string | symbol, listener: (...args: any[]) => void): this; | ||
mount(middleware: WebMiddlewareToken | null): void; | ||
declare module 'node:http' { | ||
type ExternalHeaders = { | ||
[K in LowerExternalStringHeaderKeys]?: string; | ||
}; | ||
interface IncomingHttpHeaders extends ExternalHeaders { | ||
} | ||
} | ||
type UpperHeaderKeys = UpperArrayHeaderKeys | UpperStringHeaderKeys; | ||
type UpperArrayHeaderKeys = 'Set-Cookie'; | ||
type UpperStringHeaderKeys = UpperExternalStringHeaderKeys | UpperOfficialStringHeaderKeys; | ||
type LowerExternalStringHeaderKeys = 'accept-encoding' | 'x-forwarded-for' | 'x-forwarded-proto' | 'x-forwarded-host' | 'x-real-ip' | 'access-control-request-private-network' | 'access-control-allow-private-network' | ':authority'; | ||
type UpperExternalStringHeaderKeys = 'Accept-Encoding' | 'X-Forwarded-For' | 'X-Forwarded-Proto' | 'X-Forwarded-Host' | 'X-Real-IP' | 'Access-Control-Request-Private-Network' | 'Access-Control-Allow-Private-Network'; | ||
type UpperOfficialStringHeaderKeys = 'Accept' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Ranges' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Age' | 'Allow' | 'Alt-Svc' | 'Authorization' | 'Cache-Control' | 'Connection' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Type' | 'Cookie' | 'Date' | 'Etag' | 'Expect' | 'Expires' | 'Forwarded' | 'From' | 'Host' | 'If-Match' | 'If-Modified-Since' | 'If-None-Match' | 'If-Unmodified-Since' | 'Last-Modified' | 'Location' | 'Origin' | 'Pragma' | 'Proxy-Authenticate' | 'Proxy-Authorization' | 'Public-Key-Pins' | 'Range' | 'Referer' | 'Retry-After' | 'Sec-Websocket-Accept' | 'Sec-Websocket-Extensions' | 'Sec-Websocket-Key' | 'Sec-Websocket-Protocol' | 'Sec-Websocket-Version' | 'Strict-Transport-Security' | 'Tk' | 'Trailer' | 'Transfer-Encoding' | 'Upgrade' | 'User-Agent' | 'Vary' | 'Via' | 'Warning' | 'WWW-Authenticate'; | ||
type Body = string | object | Stream | Buffer | null; | ||
interface CookieCache { | ||
set(name: string, value: string, options?: CookieSerializeOptions): void; | ||
remove(name: string, options?: Omit<CookieSerializeOptions, 'maxAge' | 'expires'>): void; | ||
} | ||
declare module WebResponse { | ||
type FakeType = typeof ServerResponse & (new (req: IncomingMessage) => ServerResponse<IncomingMessage>); | ||
} | ||
declare class WebResponse<Request extends WebRequest = WebRequest> extends ServerResponse<Request> { | ||
declare class WebResponse { | ||
app: WebApp; | ||
res: ServerResponse; | ||
ctx: WebContext; | ||
req: WebRequest; | ||
statusCode: number; | ||
statusMessage: string; | ||
private _body; | ||
/** | ||
* 是否明确设置过内容 | ||
*/ | ||
private _explicitBody; | ||
/** | ||
* 是否明确设置过状态码 | ||
*/ | ||
private _explicitStatus; | ||
private _statusCode; | ||
private _determineHeaders; | ||
private _determineNullBody; | ||
private _cookie; | ||
constructor(req: Request); | ||
readonly setHeader: { | ||
<T extends WebResponse>(name: UpperStringHeaderKeys, value: number | string): T; | ||
<T extends WebResponse>(name: UpperArrayHeaderKeys, value: readonly string[]): T; | ||
<T extends WebResponse>(name: string, value: number | string | readonly string[]): T; | ||
}; | ||
readonly getHeader: { | ||
private _updatingBodyType; | ||
constructor(req: IncomingMessage); | ||
get contentLength(): number; | ||
set contentLength(length: number); | ||
get contentType(): string; | ||
set contentType(typeOrFilenameOrExt: string); | ||
get body(): Body; | ||
set body(val: Body); | ||
download(filePath: string, options?: contentDisposition.Options): void; | ||
getHeader: { | ||
(name: UpperStringHeaderKeys): string | undefined; | ||
@@ -111,12 +130,20 @@ (name: UpperArrayHeaderKeys): string[] | undefined; | ||
}; | ||
readonly hasHeader: { | ||
(name: UpperStringHeaderKeys): boolean; | ||
(name: UpperArrayHeaderKeys): boolean; | ||
(name: string): boolean; | ||
getHeaderNames: () => (UpperStringHeaderKeys | UpperArrayHeaderKeys | (string & {}))[]; | ||
getHeaders: () => OutgoingHttpHeaders; | ||
flush(): void; | ||
hasHeader: (name: UpperStringHeaderKeys | UpperArrayHeaderKeys | (string & {})) => boolean; | ||
isJSON(body: Body): body is object; | ||
matchContentType(type: string, ...types: string[]): string | null; | ||
onError(error?: Error | HttpError | null): void; | ||
redirect(url: string): void; | ||
redirect(statusCode: 300 | 301 | 302 | 303 | 305 | 307 | 308, url: string): void; | ||
removeCookie(name: string, options?: Pick<CookieSerializeOptions, 'path'>): void; | ||
removeHeader: (name: UpperStringHeaderKeys | UpperArrayHeaderKeys | (string & {})) => void; | ||
removeHeaders(...headers: ((string & {}) | UpperStringHeaderKeys | UpperArrayHeaderKeys)[]): void; | ||
setCookie(name: string, value: string, options?: CookieSerializeOptions): void; | ||
setHeader: { | ||
<T extends WebResponse>(name: UpperStringHeaderKeys, value: number | string): T; | ||
<T extends WebResponse>(name: UpperArrayHeaderKeys, value: readonly string[]): T; | ||
<T extends WebResponse>(name: string, value: number | string | readonly string[]): T; | ||
}; | ||
readonly removeHeader: { | ||
(name: UpperStringHeaderKeys): void; | ||
(name: UpperArrayHeaderKeys): void; | ||
(name: string): void; | ||
}; | ||
setHeaders(headers: { | ||
@@ -129,96 +156,7 @@ [K: string]: string | number | readonly string[]; | ||
}): void; | ||
protected removeHeaders(...headers: Array<UpperStringHeaderKeys | UpperArrayHeaderKeys>): void; | ||
protected removeHeaders(...headers: Array<string>): void; | ||
redirect(url: string): void; | ||
redirect(statusCode: 300 | 301 | 302 | 303 | 305 | 307 | 308, url: string): void; | ||
download(filePath: string, options?: contentDisposition.Options): void; | ||
get contentLength(): number; | ||
set contentLength(length: number); | ||
get contentType(): string; | ||
set contentType(typeOrFilenameOrExt: string); | ||
get body(): Body; | ||
set body(val: Body); | ||
isJSON(body: Body): body is object; | ||
flush(): any; | ||
onError(error?: Error | HttpError | null): void; | ||
findContentType(type: string, ...types: string[]): string | null; | ||
vary(field: UpperHeaderKeys | UpperHeaderKeys[]): void; | ||
vary(field: string | string[]): void; | ||
varyAppend(header: UpperHeaderKeys, field: UpperHeaderKeys | UpperHeaderKeys[]): string; | ||
varyAppend(header: UpperHeaderKeys, field: string | string[]): string; | ||
varyAppend(header: string, field: UpperHeaderKeys | UpperHeaderKeys[]): string; | ||
varyAppend(header: string, field: string | string[]): string; | ||
get cookie(): CookieCache; | ||
vary(field: UpperHeaderKeys | UpperHeaderKeys[] | (string & {}) | (string & {})[]): string; | ||
protected setStatus(code: number): void; | ||
protected determineHeaders(): void; | ||
protected determineNullBody(): void; | ||
protected updateBodyType(): void; | ||
} | ||
declare const getMimeType: (filenameOrExt: string) => string | false; | ||
declare enum METHOD { | ||
GET = "GET", | ||
POST = "POST", | ||
PUT = "PUT", | ||
PATCH = "PATCH", | ||
DELETE = "DELETE", | ||
OPTIONS = "OPTIONS", | ||
HEAD = "HEAD" | ||
} | ||
declare const createHttpServer: (listener: RequestListener$1<typeof WebRequest, | ||
/** @ts-ignore */ | ||
typeof WebResponse<WebRequest>>) => Server$1<any, any>; | ||
declare class WebRequest extends IncomingMessage { | ||
app: WebApp; | ||
res: WebResponse; | ||
ctx: WebContext; | ||
method: METHOD; | ||
url: string; | ||
params: Record<string, unknown>; | ||
protected _query?: any; | ||
protected _body?: any; | ||
protected _accept?: Accepts; | ||
protected _cookie?: { | ||
readonly [key: string]: string | undefined; | ||
}; | ||
protected _parsedUrl: URL | null; | ||
get pathname(): string; | ||
/** | ||
* 搜索字符串,比查询字符串多了一个开头问号(?) | ||
*/ | ||
get search(): string; | ||
get querystring(): string; | ||
get query(): Record<string, unknown>; | ||
get body(): Promise<unknown>; | ||
get contentType(): string; | ||
findContentType(type: string, ...types: string[]): string | null; | ||
get accept(): Accepts; | ||
get ip(): string; | ||
/** | ||
* 包括了协议,域名,端口和路径的完整链接 | ||
*/ | ||
get href(): string; | ||
get origin(): string; | ||
get host(): string; | ||
get protocol(): string; | ||
get secure(): boolean; | ||
get fresh(): boolean; | ||
/** | ||
* 把请求头部的`Cookie`字段解析成对象格式 | ||
*/ | ||
get cookie(): { | ||
readonly [key: string]: string | undefined; | ||
}; | ||
protected get URL(): URL; | ||
} | ||
declare module 'node:http' { | ||
type ExternalHeaders = { | ||
[K in LowerExternalStringHeaderKeys]?: string; | ||
}; | ||
interface IncomingHttpHeaders extends ExternalHeaders { | ||
} | ||
} | ||
declare class WebContext { | ||
@@ -229,2 +167,12 @@ readonly app: WebApp; | ||
constructor(app: WebApp, request: WebRequest, response: WebResponse); | ||
/** | ||
* 发送响应内容和状态,包含字符串,对象,数据流,缓冲区等。 | ||
* ```typescript | ||
* ctx.send(200, 'hello aomex'); | ||
* ctx.send({ count: 1 }); | ||
* ``` | ||
* 更多快捷方式: | ||
* - 链接重定向:`ctx.response.redirect(...)` | ||
* - 下载文件:`ctx.response.download(...)` | ||
*/ | ||
send(statusCode: number, body?: Body): this; | ||
@@ -248,40 +196,62 @@ send(body: Body): this; | ||
*/ | ||
throw(statusCode: number, message?: string | Error, properties?: {}): never; | ||
throw(message: string | Error, statusCode?: number, properties?: {}): never; | ||
throw(arg: any, ...properties: Array<number | string | {}>): never; | ||
throw(statusCode: number, message?: string | Error, properties?: HttpErrorProperties): never; | ||
throw(message: string | Error, statusCode?: number, properties?: HttpErrorProperties): never; | ||
throw(statusCode?: number, properties?: HttpErrorProperties): never; | ||
} | ||
interface WebMiddlewareSkipOptions { | ||
custom?: (ctx: WebContext) => boolean | Promise<boolean>; | ||
path?: string | RegExp | (string | RegExp)[]; | ||
ext?: string | string[]; | ||
method?: string | string[]; | ||
interface HttpErrorProperties { | ||
expose?: boolean; | ||
statusCode?: number; | ||
code?: string; | ||
headers?: Record<string, any>; | ||
cause?: any; | ||
[key: string]: any; | ||
} | ||
/** | ||
* 跳过中间件或者链条 | ||
* - 通过条件判断 - options=`object|function` | ||
* - 不跳过 - options=`false` | ||
* - 总是跳过 - options=`true` | ||
*/ | ||
declare function skip<Props extends object, T extends WebMiddlewareSkipOptions | NonNullable<WebMiddlewareSkipOptions['custom']> | boolean>(token: WebMiddlewareToken<Props>, options: T): WebMiddleware<T extends false ? Props : T extends true ? object : Partial<Props>>; | ||
declare module '@aomex/core' { | ||
interface MiddlewarePlatform { | ||
readonly web: <Props extends object = object>(fn: (ctx: NonReadonly<Props> & WebContext, next: Next) => any) => WebMiddleware<Props>; | ||
readonly web: <Props extends object = object>(fn: WebMiddlewareArguments<Props>['fn'] | WebMiddlewareArguments<Props>) => WebMiddleware<Props>; | ||
} | ||
} | ||
interface WebMiddlewareToDocument { | ||
document: OpenAPI.Document; | ||
pathItem?: OpenAPI.PathItemObject; | ||
methodName?: METHOD; | ||
methodItem?: OpenAPI.OperationObject; | ||
interface OpenApiInjector { | ||
onDocument?: (document: OpenAPI.Document) => void; | ||
postDocument?: OpenApiInjector['onDocument']; | ||
onPath?: (pathItem: OpenAPI.PathItemObject, opts: { | ||
document: OpenAPI.Document; | ||
pathName: string; | ||
}) => void; | ||
postPath?: OpenApiInjector['onPath']; | ||
onMethod?: (methodItem: OpenAPI.OperationObject, opts: { | ||
document: OpenAPI.Document; | ||
pathName: string; | ||
pathItem: OpenAPI.PathItemObject; | ||
methodName: `${Lowercase<WebRequest['method']>}`; | ||
}) => void; | ||
postMethod?: OpenApiInjector['onMethod']; | ||
} | ||
interface WebMiddlewareArguments<T extends object> { | ||
fn: (ctx: NonReadonly<T> & WebContext, next: Next) => any; | ||
openapi?: OpenApiInjector; | ||
} | ||
declare class WebMiddleware<Props extends object = object> extends Middleware<Props> { | ||
_contextType: WebContext; | ||
protected _web_middleware_: 'web-middleware'; | ||
constructor(fn: (ctx: NonReadonly<Props> & WebContext, next: Next) => any); | ||
skipIf(options: WebMiddlewareSkipOptions | NonNullable<WebMiddlewareSkipOptions['custom']>): WebMiddleware<Partial<Props>>; | ||
toDocument(options: WebMiddlewareToDocument): void; | ||
private readonly openapiInjector; | ||
constructor(args: WebMiddlewareArguments<Props>['fn'] | WebMiddlewareArguments<Props>); | ||
protected openapi(): OpenApiInjector; | ||
} | ||
declare module '@aomex/core' { | ||
interface MiddlewareChainPlatform { | ||
readonly web: WebMiddlewareChain; | ||
} | ||
} | ||
type WebMiddlewareToken<P extends object = object> = WebMiddleware<P> | WebMiddlewareChain<P> | MixinMiddlewareToken<P>; | ||
declare class WebMiddlewareChain<Props extends object = object> extends MiddlewareChain<Props> { | ||
protected _web_chain_: 'web-chain'; | ||
mount: { | ||
<P extends object>(middleware: WebMiddlewareToken<P> | null): WebMiddlewareChain<Props & P>; | ||
}; | ||
} | ||
declare module '@aomex/core' { | ||
interface Rule { | ||
@@ -325,3 +295,3 @@ file(): FileValidator; | ||
protected isEmpty(value: any): boolean; | ||
protected validateValue(file: FileValidator.FormidableFile, key: string, superKeys: string[]): magistrate.Result<FileValidator.FormidableFile>; | ||
protected validateValue(file: FileValidator.FormidableFile, _key: string, label: string): magistrate.Result<FileValidator.FormidableFile>; | ||
protected copy: () => FileValidator<T>; | ||
@@ -331,13 +301,33 @@ protected toDocument(): OpenAPI.SchemaObject; | ||
declare class WebBodyMiddleware<Props extends { | ||
[key: string]: Validator; | ||
}> extends WebMiddleware<{ | ||
readonly body: { | ||
[K in keyof Props]: Validator.Infer<Props[K]>; | ||
}; | ||
interface WebAppOption { | ||
/** | ||
* 调试模式。默认值:`process.env.NODE_ENV !== 'production'` | ||
*/ | ||
debug?: boolean; | ||
/** | ||
* 全局中间件组,挂载后该组会被打上标记。 | ||
* ```typescript | ||
* const appChain = mdchain.web.mount(md1).mount(md2); | ||
* const chain1 = appChain.mount(md3); | ||
* const chain2 = chain1.mount(md4); | ||
* | ||
* const app = new WebApp({ box: appChain }); | ||
* ``` | ||
*/ | ||
mount?: WebMiddlewareChain | MixinMiddlewareChain; | ||
locale?: I18n.LocaleName; | ||
} | ||
declare class WebApp extends EventEmitter<{ | ||
error: [err: HttpError, ctx: WebContext]; | ||
}> { | ||
protected readonly props: Props; | ||
constructor(props: Props); | ||
toDocument(options: WebMiddlewareToDocument): void; | ||
protected readonly options: WebAppOption; | ||
protected readonly point?: string; | ||
protected readonly middlewareList: Middleware[]; | ||
constructor(options?: WebAppOption); | ||
get debug(): boolean; | ||
callback(): RequestListener<any, any>; | ||
listen: Server['listen']; | ||
log(err: HttpError, ctx: WebContext): void; | ||
} | ||
/** | ||
@@ -361,15 +351,6 @@ * 验证请求实体 | ||
[key: string]: P; | ||
}, P extends Validator<unknown>>(fields: T) => WebBodyMiddleware<T>; | ||
}, P extends Validator<unknown>>(fields: T) => WebMiddleware<{ | ||
readonly body: Validator.Infer<T>; | ||
}>; | ||
declare class WebQueryMiddleware<Props extends { | ||
[key: string]: Validator; | ||
}> extends WebMiddleware<{ | ||
readonly query: { | ||
[K in keyof Props]: Validator.Infer<Props[K]>; | ||
}; | ||
}> { | ||
protected readonly props: Props; | ||
constructor(props: Props); | ||
toDocument(options: WebMiddlewareToDocument): void; | ||
} | ||
/** | ||
@@ -392,15 +373,6 @@ * 验证请求查询字符串 | ||
[key: string]: P; | ||
}, P extends Validator<unknown>>(fields: T) => WebQueryMiddleware<T>; | ||
}, P extends Validator<unknown>>(fields: T) => WebMiddleware<{ | ||
readonly query: Validator.Infer<T>; | ||
}>; | ||
declare class WebParamMiddleware<Props extends { | ||
[key: string]: Validator; | ||
}> extends WebMiddleware<{ | ||
readonly params: { | ||
[K in keyof Props]: Validator.Infer<Props[K]>; | ||
}; | ||
}> { | ||
protected readonly props: Props; | ||
constructor(props: Props); | ||
toDocument(options: WebMiddlewareToDocument): void; | ||
} | ||
/** | ||
@@ -422,17 +394,11 @@ * 验证请求路径参数 | ||
[key: string]: P; | ||
}, P extends Validator<unknown>>(fields: T) => WebParamMiddleware<T>; | ||
}, P extends Validator<unknown>>(fields: T) => WebMiddleware<{ | ||
readonly params: Validator.Infer<T>; | ||
}>; | ||
interface WebResponseOptions extends Pick<OpenAPI.MediaTypeObject, 'example'> { | ||
statusCode: number; | ||
/** | ||
* 可选格式: | ||
* - 200 | ||
* - 201 | ||
* - 2xx | ||
* - 3xx | ||
* - 20x | ||
* - 40x | ||
* - default (handle unknown response) | ||
*/ | ||
statusCode: number | string; | ||
/** | ||
* 响应格式,支持简写和完整写法。如果不填,则根据响应值判断 | ||
* | ||
* - json | ||
@@ -444,9 +410,9 @@ * - application/json | ||
* - stream | ||
* - \*\/\* (any type) | ||
* - \*\/\* | ||
*/ | ||
contentType?: string; | ||
/** | ||
* 最终的响应格式 | ||
* 最终的响应值类型 | ||
*/ | ||
schema?: CompatibleValidator; | ||
schema?: ValidatorToken; | ||
headers?: { | ||
@@ -456,27 +422,14 @@ [key: string]: Validator; | ||
description?: string; | ||
/** | ||
* 验证响应数据。 | ||
* | ||
* 如果未设置,则查看`app.options.validateResponse`。仍未设置,则查看`app.options.debug` | ||
* | ||
* ```typescript | ||
* const app = new WebApp({ | ||
* validateResponse: false | ||
* }) | ||
* ``` | ||
*/ | ||
validate?: boolean; | ||
} | ||
/** | ||
* 为openapi生成响应数据文档,并提供运行时验证(严格模式) | ||
*/ | ||
declare const response: (options: WebResponseOptions) => WebResponseMiddleware; | ||
declare class WebResponseMiddleware extends WebMiddleware<object> { | ||
protected readonly options: WebResponseOptions; | ||
constructor(options: WebResponseOptions); | ||
matchStatusCode(expected: number | string, statusCode: number): boolean; | ||
toDocument({ methodItem }: WebMiddlewareToDocument): void; | ||
protected fixContentType(contentType: string): string; | ||
protected openapi(): OpenApiInjector; | ||
protected getContentType(): string; | ||
} | ||
/** | ||
* 为openapi生成响应数据文档 | ||
*/ | ||
declare const response: (options: WebResponseOptions) => WebResponseMiddleware; | ||
export { Body, FileValidator, METHOD, WebApp, WebAppOption, WebBodyMiddleware, WebChain, WebContext, WebMiddleware, WebMiddlewareSkipOptions, WebMiddlewareToDocument, WebMiddlewareToken, WebParamMiddleware, WebQueryMiddleware, WebRequest, WebResponse, WebResponseMiddleware, WebResponseOptions, body, createHttpServer, getMimeType, params, query, response, skip }; | ||
export { type Body, FileValidator, type OpenApiInjector, WebApp, type WebAppOption, WebContext, WebMiddleware, WebMiddlewareChain, type WebMiddlewareToken, WebRequest, WebResponse, WebResponseMiddleware, type WebResponseOptions, body, params, query, response }; |
1117
dist/index.js
@@ -1,239 +0,71 @@ | ||
// src/override/middleware.ts | ||
import { Middleware as Middleware2 } from "@aomex/core"; | ||
// src/middleware/skip.ts | ||
import { extname } from "node:path"; | ||
import { compose, Middleware, middleware } from "@aomex/core"; | ||
import { toArray } from "@aomex/internal-tools"; | ||
function skip(token, options) { | ||
if (options === true) { | ||
return middleware.web((_, next) => next()); | ||
} | ||
const originFn = token instanceof Middleware ? token.fn : compose(toArray(token)); | ||
if (options === false) { | ||
return middleware.web(originFn); | ||
} | ||
const opts = typeof options === "function" ? { custom: options } : options; | ||
return middleware.web(async (ctx, next) => { | ||
const skipped = await shouldSkip(ctx, opts); | ||
return skipped ? next() : originFn(ctx, next); | ||
}); | ||
} | ||
var shouldSkip = async (ctx, options) => { | ||
if (await options.custom?.(ctx)) | ||
return true; | ||
if (options.path) { | ||
const { pathname } = ctx.request; | ||
return toArray(options.path).some( | ||
(path) => typeof path === "string" ? path === pathname : path.exec(pathname) !== null | ||
); | ||
} | ||
if (options.ext) { | ||
const currentExt = extname(ctx.request.pathname); | ||
if (currentExt === "") | ||
return false; | ||
return toArray(options.ext).some((ext) => { | ||
return currentExt === (ext.startsWith(".") ? ext : `.${ext}`); | ||
}); | ||
} | ||
if (options.method) { | ||
return toArray(options.method).includes(ctx.request.method); | ||
} | ||
return false; | ||
}; | ||
// src/override/middleware.ts | ||
var WebMiddleware = class extends Middleware2 { | ||
constructor(fn) { | ||
super(fn); | ||
} | ||
skipIf(options) { | ||
return skip(this, options); | ||
} | ||
toDocument(options) { | ||
options; | ||
} | ||
}; | ||
Middleware2.register("web", WebMiddleware); | ||
// src/override/chain.ts | ||
import { Chain } from "@aomex/core"; | ||
var WebChain = class extends Chain { | ||
}; | ||
Chain.register("web", WebChain); | ||
// src/override/file-validator.ts | ||
import { PersistentFile } from "formidable"; | ||
import mimeTypes from "mime-types"; | ||
import { | ||
Validator, | ||
magistrate, | ||
Rule | ||
} from "@aomex/core"; | ||
import { bytes } from "@aomex/internal-tools"; | ||
import typeIs from "type-is"; | ||
var FileValidator = class extends Validator { | ||
/** | ||
* 允许的最大体积 | ||
* | ||
* 可选格式: | ||
* - 1024 | ||
* - 2048 | ||
* - '15KB' | ||
* - '20MB' | ||
*/ | ||
maxSize(byte) { | ||
const validator = this.copy(); | ||
validator.config.maxSize = typeof byte === "number" ? byte : bytes(byte); | ||
return validator; | ||
} | ||
mimeTypes(mineOrExt, ...others) { | ||
const validator = this.copy(); | ||
validator.config.mimeTypes = [ | ||
...new Set( | ||
[].concat(mineOrExt).concat(others).map(mimeTypes.contentType).filter(Boolean) | ||
) | ||
]; | ||
return validator; | ||
} | ||
isEmpty(value) { | ||
return super.isEmpty(value) || Array.isArray(value) && !value.length; | ||
} | ||
validateValue(file, key, superKeys) { | ||
const { maxSize, mimeTypes: mimeTypes3 } = this.config; | ||
if (Array.isArray(file)) { | ||
if (file.length > 1) { | ||
return magistrate.fail( | ||
"use rule.array(rule.file()) for multiple files", | ||
key, | ||
superKeys | ||
); | ||
} else { | ||
file = file[0]; | ||
} | ||
// src/i18n/locales/zh-cn.ts | ||
import { i18n } from "@aomex/core"; | ||
i18n.register("zh_CN", "web", { | ||
validator: { | ||
file: { | ||
must_be_file: "{{label}}\uFF1A\u5FC5\u987B\u662F\u6587\u4EF6\u7C7B\u578B", | ||
too_large: "{{label}}\uFF1A\u6587\u4EF6\u4F53\u79EF\u592A\u5927", | ||
unsupported_mimetype: "{{label}}\uFF1A\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B" | ||
} | ||
if (!(file instanceof PersistentFile)) { | ||
return magistrate.fail("must be file", key, superKeys); | ||
} | ||
if (maxSize !== void 0 && file.size > maxSize) { | ||
return magistrate.fail("file size is too large", key, superKeys); | ||
} | ||
const hasMimeTypeLimitation = mimeTypes3 && mimeTypes3.length; | ||
if (hasMimeTypeLimitation && (!file.mimetype || !typeIs.is(file.mimetype, ...mimeTypes3))) { | ||
return magistrate.fail("file not match mime-types", key, superKeys); | ||
} | ||
return magistrate.ok(file); | ||
} | ||
toDocument() { | ||
return { | ||
type: "string", | ||
format: "binary" | ||
}; | ||
} | ||
}; | ||
Rule.register("file", FileValidator); | ||
}); | ||
// src/app/context.ts | ||
import createHttpError from "http-errors"; | ||
var WebContext = class { | ||
constructor(app, request, response2) { | ||
this.app = app; | ||
this.request = request; | ||
this.response = response2; | ||
request.app = response2.app = app; | ||
request.res = response2; | ||
request.ctx = response2.ctx = this; | ||
} | ||
send(statusOrBody, body2) { | ||
if (typeof statusOrBody === "number") { | ||
this.response.statusCode = statusOrBody; | ||
} else { | ||
body2 = statusOrBody; | ||
// src/i18n/locales/en-us.ts | ||
import { i18n as i18n2 } from "@aomex/core"; | ||
i18n2.register("en_US", "web", { | ||
validator: { | ||
file: { | ||
must_be_file: "{{label}}: must be file", | ||
too_large: "{{label}}: file size too large", | ||
unsupported_mimetype: "{{label}}: unsupported file mimetype" | ||
} | ||
if (body2 !== void 0) { | ||
this.response.body = body2; | ||
} | ||
return this; | ||
} | ||
throw(arg, ...args) { | ||
throw createHttpError(arg, ...args); | ||
} | ||
}; | ||
// src/app/app.ts | ||
import EventEmitter from "node:events"; | ||
import { EOL } from "node:os"; | ||
import { Chain as Chain2, compose as compose2 } from "@aomex/core"; | ||
import { chalk } from "@aomex/internal-tools"; | ||
// src/util/get-content-type.ts | ||
import mimeTypes2 from "mime-types"; | ||
import { LRUCache } from "lru-cache"; | ||
var cache = new LRUCache({ | ||
max: 100 | ||
}); | ||
var getMimeType = (filenameOrExt) => { | ||
let mimeType = cache.get(filenameOrExt); | ||
if (!mimeType) { | ||
mimeType = mimeTypes2.contentType(filenameOrExt) || ""; | ||
cache.set(filenameOrExt, mimeType); | ||
} | ||
return mimeType; | ||
}; | ||
// src/util/method.ts | ||
var METHOD = /* @__PURE__ */ ((METHOD2) => { | ||
METHOD2["GET"] = "GET"; | ||
METHOD2["POST"] = "POST"; | ||
METHOD2["PUT"] = "PUT"; | ||
METHOD2["PATCH"] = "PATCH"; | ||
METHOD2["DELETE"] = "DELETE"; | ||
METHOD2["OPTIONS"] = "OPTIONS"; | ||
METHOD2["HEAD"] = "HEAD"; | ||
return METHOD2; | ||
})(METHOD || {}); | ||
// src/http/app.ts | ||
import { createServer } from "node:http"; | ||
import { EventEmitter } from "node:stream"; | ||
// src/util/create-http-server.ts | ||
import { createServer } from "http"; | ||
// src/app/request.ts | ||
// src/http/request.ts | ||
import { IncomingMessage } from "node:http"; | ||
import qs from "qs"; | ||
import cookie from "cookie"; | ||
import formidable from "formidable"; | ||
import coBody from "co-body"; | ||
import formidable from "formidable"; | ||
import accepts from "accepts"; | ||
import typeIs2 from "type-is"; | ||
import contentType from "content-type"; | ||
import typeIs from "type-is"; | ||
import requestIP from "request-ip"; | ||
import fresh from "fresh"; | ||
import contentType from "content-type"; | ||
import accepts from "accepts"; | ||
import cookie from "cookie"; | ||
var WebRequest = class extends IncomingMessage { | ||
app; | ||
req; | ||
res; | ||
ctx; | ||
params = {}; | ||
_query; | ||
_accept; | ||
_cookies; | ||
_body; | ||
_accept; | ||
_cookie; | ||
_parsedUrl = null; | ||
get pathname() { | ||
return this.URL.pathname; | ||
_query; | ||
get accept() { | ||
return this._accept || (this._accept = accepts(this)); | ||
} | ||
get contentType() { | ||
try { | ||
return contentType.parse(this).type; | ||
} catch { | ||
return ""; | ||
} | ||
} | ||
/** | ||
* 搜索字符串,比查询字符串多了一个开头问号(?) | ||
* 返回从`headers['cookie']`解析后的cookie列表 | ||
*/ | ||
get search() { | ||
return this.URL.search; | ||
get cookies() { | ||
return this._cookies ||= cookie.parse(this.headers["cookie"] || ""); | ||
} | ||
get querystring() { | ||
return this.URL.search.slice(1); | ||
} | ||
get query() { | ||
return this._query ||= qs.parse(this.querystring, this.app.options.query); | ||
} | ||
get body() { | ||
if (this._body) | ||
return Promise.resolve(this._body); | ||
if (this.findContentType("multipart/*") !== null) { | ||
if (this.matchContentType("multipart/*") !== null) { | ||
const form = formidable({ | ||
@@ -248,6 +80,3 @@ hashAlgorithm: "md5", | ||
Object.entries(fields).map(([key, values]) => { | ||
return [ | ||
key, | ||
values == void 0 || values.length > 1 ? values : values[0] | ||
]; | ||
return [key, values == void 0 || values.length > 1 ? values : values[0]]; | ||
}) | ||
@@ -264,19 +93,22 @@ ); | ||
} | ||
get contentType() { | ||
try { | ||
return contentType.parse(this).type; | ||
} catch { | ||
return ""; | ||
get fresh() { | ||
if (this.method !== "GET" && this.method !== "HEAD") | ||
return false; | ||
const status = this.res.statusCode; | ||
if (status < 200) | ||
return false; | ||
if (status >= 300 && status !== 304) | ||
return false; | ||
return fresh(this.headers, this.res.getHeaders()); | ||
} | ||
get host() { | ||
let host = this.headers["x-forwarded-host"]; | ||
if (!host) { | ||
if (this.httpVersionMajor >= 2) { | ||
host = this.headers[":authority"]; | ||
} | ||
host ||= this.headers["host"]; | ||
} | ||
return host && host.split(/\s*,\s*/, 1)[0] || ""; | ||
} | ||
findContentType(type, ...types) { | ||
const result = typeIs2(this, type, ...types); | ||
return result === false ? null : result; | ||
} | ||
get accept() { | ||
return this._accept || (this._accept = accepts(this)); | ||
} | ||
get ip() { | ||
return requestIP.getClientIp(this) || ""; | ||
} | ||
/** | ||
@@ -288,14 +120,10 @@ * 包括了协议,域名,端口和路径的完整链接 | ||
} | ||
get ip() { | ||
return requestIP.getClientIp(this) || ""; | ||
} | ||
get origin() { | ||
return `${this.protocol}://${this.host}`; | ||
} | ||
get host() { | ||
let host = this.headers["x-forwarded-host"]; | ||
if (!host) { | ||
if (this.httpVersionMajor >= 2) { | ||
host = this.headers[":authority"]; | ||
} | ||
host ||= this.headers["host"]; | ||
} | ||
return host && host.split(/\s*,\s*/, 1)[0] || ""; | ||
get pathname() { | ||
return this.URL.pathname; | ||
} | ||
@@ -308,118 +136,83 @@ get protocol() { | ||
} | ||
get secure() { | ||
return this.protocol === "https"; | ||
/** | ||
* 查询字符串对象 | ||
*/ | ||
get query() { | ||
return this._query ||= qs.parse(this.querystring); | ||
} | ||
get fresh() { | ||
const method = this.method; | ||
const status = this.res.statusCode; | ||
if (method !== "GET" && method !== "HEAD") | ||
return false; | ||
if (status < 200) | ||
return false; | ||
if (status > 299 && status !== 304) | ||
return false; | ||
return fresh(this.headers, this.res.getHeaders()); | ||
/** | ||
* 查询字符串 | ||
*/ | ||
get querystring() { | ||
return this.URL.search.slice(1); | ||
} | ||
/** | ||
* 把请求头部的`Cookie`字段解析成对象格式 | ||
* 搜索字符串,比查询字符串多了一个开头问号(?) | ||
*/ | ||
get cookie() { | ||
return this._cookie ||= cookie.parse( | ||
this.headers["cookie"] || "", | ||
this.app.options.cookie?.get | ||
); | ||
get search() { | ||
return this.URL.search; | ||
} | ||
get secure() { | ||
return this.protocol === "https"; | ||
} | ||
get URL() { | ||
return this._parsedUrl ||= new URL(this.href); | ||
} | ||
matchContentType(type, ...types) { | ||
const result = typeIs(this, type, ...types); | ||
return result === false ? null : result; | ||
} | ||
}; | ||
// src/util/create-http-server.ts | ||
var createHttpServer = (listener) => { | ||
return createServer( | ||
{ | ||
IncomingMessage: WebRequest, | ||
// @ts-ignore | ||
ServerResponse: WebResponse | ||
}, | ||
listener | ||
); | ||
}; | ||
// src/http/response.ts | ||
import { ServerResponse } from "node:http"; | ||
import assert from "node:assert"; | ||
import statuses from "statuses"; | ||
import { Stream } from "node:stream"; | ||
// src/app/app.ts | ||
var WebApp = class extends EventEmitter { | ||
constructor(options = {}) { | ||
super(); | ||
this.options = options; | ||
// src/utils/get-mime-type.ts | ||
import mimeTypes from "mime-types"; | ||
import { LRUCache } from "lru-cache"; | ||
var cache = new LRUCache({ | ||
max: 100 | ||
}); | ||
var getMimeType = (filenameOrExt) => { | ||
let mimeType = cache.get(filenameOrExt); | ||
if (!mimeType) { | ||
mimeType = mimeTypes.contentType(filenameOrExt) || ""; | ||
cache.set(filenameOrExt, mimeType); | ||
} | ||
chainPoints = []; | ||
middlewareList = []; | ||
get debug() { | ||
return this.options.debug ?? process.env["NODE_ENV"] !== "production"; | ||
} | ||
listen = (...args) => { | ||
const server = createHttpServer(this.callback()); | ||
return server.listen(...args); | ||
}; | ||
callback() { | ||
const fn = compose2(this.middlewareList); | ||
if (!this.listenerCount("error")) { | ||
this.on("error", this.log.bind(this)); | ||
} | ||
return (req, res) => { | ||
return fn(new WebContext(this, req, res)).then(res.flush.bind(res)).catch(res.onError); | ||
}; | ||
} | ||
log(err) { | ||
if (this.options.silent) | ||
return; | ||
if ((err.status || err.statusCode) === 404 || err.expose) | ||
return; | ||
const msgs = (err.stack || err.toString()).split(EOL, 2); | ||
console.error( | ||
["", chalk.bgRed(msgs.shift()), msgs.join(EOL), ""].join(EOL) | ||
); | ||
} | ||
on(eventName, listener) { | ||
return super.on(eventName, listener); | ||
} | ||
mount(middleware2) { | ||
if (middleware2 === null) | ||
return; | ||
if (middleware2 instanceof Chain2) { | ||
this.chainPoints.push(Chain2.createSplitPoint(middleware2)); | ||
} | ||
this.middlewareList.push(middleware2); | ||
} | ||
return mimeType; | ||
}; | ||
// src/app/response.ts | ||
import { ServerResponse } from "node:http"; | ||
import stream, { Stream } from "node:stream"; | ||
import assert from "node:assert"; | ||
import statuses from "statuses"; | ||
import escapeHtml from "escape-html"; | ||
import encodeUrl from "encodeurl"; | ||
// src/http/response.ts | ||
import contentType2 from "content-type"; | ||
import stream from "stream"; | ||
import destroy from "destroy"; | ||
import createHttpError, { isHttpError } from "http-errors"; | ||
import encodeUrl from "encodeurl"; | ||
import escapeHtml from "escape-html"; | ||
import contentDisposition from "content-disposition"; | ||
import createHttpError2, { isHttpError } from "http-errors"; | ||
import { extname as extname2 } from "node:path"; | ||
import { extname } from "node:path"; | ||
import { createReadStream } from "node:fs"; | ||
import typeIs3 from "type-is"; | ||
import vary from "vary"; | ||
import typeIs2 from "type-is"; | ||
import cookie2 from "cookie"; | ||
var WebResponse = class extends ServerResponse { | ||
app; | ||
res; | ||
ctx; | ||
_body = null; | ||
/** | ||
* 是否明确设置过内容 | ||
*/ | ||
_explicitBody = false; | ||
/** | ||
* 是否明确设置过状态码 | ||
*/ | ||
_explicitStatus = false; | ||
_statusCode; | ||
_determineHeaders = false; | ||
_determineNullBody = false; | ||
_cookie = null; | ||
_statusCode = 404; | ||
_updatingBodyType = false; | ||
constructor(req) { | ||
super(req); | ||
this._statusCode = 404; | ||
Object.defineProperty(this, "statusCode", { | ||
@@ -434,32 +227,4 @@ get: () => { | ||
this.onError = this.onError.bind(this); | ||
this.flush = this.flush.bind(this); | ||
} | ||
setHeaders(headers) { | ||
for (const [key, value] of Object.entries(headers)) { | ||
value !== void 0 && this.setHeader(key, value); | ||
} | ||
} | ||
removeHeaders(...headers) { | ||
headers.forEach(this.removeHeader.bind(this)); | ||
} | ||
redirect(status, url) { | ||
url = typeof url === "string" ? url : status.toString(); | ||
this.statusCode = typeof status === "string" ? 302 : status; | ||
this.setHeader("location", encodeUrl(url)); | ||
if (this.req.accept.types("html")) { | ||
url = escapeHtml(url); | ||
this.contentType = "html"; | ||
this.body = `Redirecting to <a href="${url}">${url}</a>.`; | ||
} else { | ||
this.contentType = "text"; | ||
this.body = `Redirecting to ${url}.`; | ||
} | ||
} | ||
download(filePath, options = {}) { | ||
this.contentType = extname2(filePath); | ||
this.setHeader( | ||
"content-disposition", | ||
contentDisposition(filePath, options) | ||
); | ||
this.body = createReadStream(filePath); | ||
} | ||
get contentLength() { | ||
@@ -480,10 +245,10 @@ const length = this.getHeader("Content-Length"); | ||
try { | ||
if (mimeType === false) | ||
throw new Error(); | ||
if (!mimeType) | ||
throw new Error(""); | ||
contentType2.parse(mimeType); | ||
} catch { | ||
throw new TypeError(`invalid content-type: '${typeOrFilenameOrExt}'`); | ||
throw new TypeError(`\u4E0D\u5408\u6CD5\u7684\u7C7B\u578B\uFF1A'${typeOrFilenameOrExt}'`); | ||
} | ||
this.setHeader("Content-Type", mimeType); | ||
this.determineNullBody(); | ||
this.updateBodyType(); | ||
} | ||
@@ -494,3 +259,3 @@ get body() { | ||
set body(val) { | ||
this._body = val; | ||
this._body = val == null ? null : val; | ||
this._explicitBody = true; | ||
@@ -500,6 +265,8 @@ if (!this._explicitStatus) { | ||
} | ||
this.determineHeaders(); | ||
this.updateBodyType(); | ||
} | ||
isJSON(body2) { | ||
return !(!body2 || typeof body2 === "string" || body2 instanceof Stream || Buffer.isBuffer(body2)); | ||
download(filePath, options = {}) { | ||
this.contentType = extname(filePath); | ||
this.setHeader("content-disposition", contentDisposition(filePath, options)); | ||
this.body = createReadStream(filePath); | ||
} | ||
@@ -509,39 +276,52 @@ flush() { | ||
return; | ||
this.determineHeaders(); | ||
let output = this.body; | ||
if (statuses.empty[this.statusCode]) | ||
return this.end(); | ||
return void this.end(); | ||
if (output === null) { | ||
const isJSON = this.contentType === "application/json"; | ||
if (this._explicitBody) { | ||
this._body = isJSON ? String(null) : ""; | ||
} else if (isJSON) { | ||
this._body = String(null); | ||
if (this.contentType === "application/json") { | ||
this.body = String(null); | ||
} else if (this._explicitBody) { | ||
this.body = ""; | ||
} else { | ||
this._body = String(this.statusMessage || this.statusCode); | ||
this.statusCode = this.statusCode; | ||
this.body = String(this.statusMessage || statuses.message[this.statusCode]); | ||
} | ||
this.determineHeaders(); | ||
output = this.body; | ||
} | ||
if (this.req.method === "HEAD") | ||
return this.end(); | ||
return void this.end(); | ||
if (typeof output === "string") | ||
return this.end(output); | ||
return void this.end(output); | ||
if (Buffer.isBuffer(output)) | ||
return this.end(output); | ||
return void this.end(output); | ||
if (output instanceof Stream) { | ||
if (!output.listenerCount("error")) { | ||
output.once("error", this.onError); | ||
} | ||
stream.finished(this, () => { | ||
destroy(output); | ||
}); | ||
if (!output.listenerCount("error")) { | ||
output.once("error", this.onError); | ||
} | ||
return output.pipe(this); | ||
return void output.pipe(this); | ||
} | ||
return this.end(JSON.stringify(output)); | ||
return void this.end(JSON.stringify(output)); | ||
} | ||
isJSON(body2) { | ||
if (!body2) | ||
return false; | ||
if (typeof body2 === "string") | ||
return false; | ||
if (body2 instanceof Stream) | ||
return false; | ||
if (Buffer.isBuffer(body2)) | ||
return false; | ||
return true; | ||
} | ||
matchContentType(type, ...types) { | ||
const result = typeIs2.is(this.contentType, type, ...types); | ||
return result === false ? null : result; | ||
} | ||
onError(error) { | ||
if (error == null) | ||
return; | ||
const err = isHttpError(error) ? error : createHttpError2(error); | ||
const err = isHttpError(error) ? error : createHttpError(error); | ||
this.removeHeaders(...this.getHeaderNames()); | ||
@@ -561,65 +341,71 @@ err.headers && this.setHeaders(err.headers); | ||
} | ||
findContentType(type, ...types) { | ||
const result = typeIs3.is(this.contentType, type, ...types); | ||
return result === false ? null : result; | ||
redirect(status, url) { | ||
url = typeof url === "string" ? url : status.toString(); | ||
this.statusCode = typeof status === "string" ? 302 : status; | ||
this.setHeader("location", encodeUrl(url)); | ||
if (this.req.accept.types("html")) { | ||
url = escapeHtml(url); | ||
this.contentType = "html"; | ||
this.body = `Redirecting to <a href="${url}">${url}</a>.`; | ||
} else { | ||
this.contentType = "text"; | ||
this.body = `Redirecting to ${url}.`; | ||
} | ||
} | ||
vary(field) { | ||
return vary(this, field); | ||
removeCookie(name, options) { | ||
return this.setCookie(name, "", { | ||
...options, | ||
maxAge: void 0, | ||
expires: /* @__PURE__ */ new Date(0) | ||
}); | ||
} | ||
varyAppend(header, field) { | ||
return vary.append(header, field); | ||
removeHeaders(...headers) { | ||
headers.forEach(this.removeHeader.bind(this)); | ||
} | ||
get cookie() { | ||
if (this._cookie) | ||
return this._cookie; | ||
const defaultSerializeOptions = { | ||
path: "/", | ||
sameSite: false, | ||
secure: this.req.secure, | ||
httpOnly: true, | ||
...this.app.options.cookie?.set | ||
}; | ||
this._cookie = { | ||
set: (name, value, options) => { | ||
const setCookie = this.getHeader("Set-Cookie") || []; | ||
setCookie.push( | ||
cookie2.serialize(name, value, { | ||
...defaultSerializeOptions, | ||
...options | ||
}) | ||
); | ||
this.setHeader("Set-Cookie", setCookie); | ||
}, | ||
remove: (name, options) => { | ||
this.cookie.set(name, "", { | ||
...defaultSerializeOptions, | ||
...options, | ||
maxAge: void 0, | ||
expires: /* @__PURE__ */ new Date(0) | ||
}); | ||
} | ||
}; | ||
return this._cookie; | ||
setCookie(name, value, options) { | ||
const cookies = this.getHeader("Set-Cookie") || []; | ||
cookies.push( | ||
cookie2.serialize(name, value, { | ||
path: "/", | ||
sameSite: true, | ||
httpOnly: true, | ||
secure: this.req.secure, | ||
...options | ||
}) | ||
); | ||
this.setHeader("Set-Cookie", cookies); | ||
} | ||
setHeaders(headers) { | ||
for (const [key, value] of Object.entries(headers)) { | ||
value !== void 0 && this.setHeader(key, value); | ||
} | ||
} | ||
vary(field) { | ||
vary(this, field); | ||
return this.getHeader("Vary"); | ||
} | ||
setStatus(code) { | ||
assert(code >= 100 && code <= 999, `invalid status code: ${code}`); | ||
assert(code >= 100 && code <= 999); | ||
this._statusCode = code; | ||
this._explicitStatus = true; | ||
if (this.req.httpVersionMajor < 2) { | ||
this.statusMessage = String(statuses.message[code]); | ||
} | ||
this.statusMessage = String(statuses.message[code] || code); | ||
if (statuses.empty[code]) { | ||
this.body = null; | ||
} else { | ||
this.determineNullBody(); | ||
this.updateBodyType(); | ||
} | ||
} | ||
determineHeaders() { | ||
if (this._determineHeaders) | ||
updateBodyType() { | ||
if (this._updatingBodyType) | ||
return; | ||
this._determineHeaders = true; | ||
this._updatingBodyType = true; | ||
const { body: body2 } = this; | ||
const missType = !this.hasHeader("Content-Type"); | ||
if (body2 === null) { | ||
this.determineNullBody(); | ||
if (statuses.empty[this.statusCode]) { | ||
this.removeHeaders("Content-Type", "Content-Length", "Transfer-Encoding"); | ||
} else if (this.contentType === "application/json") { | ||
this.contentLength = Buffer.byteLength(String(null)); | ||
} else { | ||
} | ||
} else if (typeof body2 === "string") { | ||
@@ -640,41 +426,189 @@ this.contentLength = Buffer.byteLength(body2); | ||
} else { | ||
this.contentType = "json"; | ||
this.contentLength = Buffer.byteLength(JSON.stringify(body2)); | ||
if (missType) { | ||
this.contentType = "json"; | ||
} | ||
} | ||
this._determineHeaders = false; | ||
this._updatingBodyType = false; | ||
} | ||
determineNullBody() { | ||
if (this._determineNullBody || this.body !== null) | ||
return; | ||
this._determineNullBody = true; | ||
if (statuses.empty[this.statusCode]) { | ||
this.removeHeaders("Content-Type", "Content-Length", "Transfer-Encoding"); | ||
} else if (this._explicitBody) { | ||
if (this.contentType === "application/json") { | ||
this.contentLength = Buffer.byteLength(String(null)); | ||
} else { | ||
this.contentLength = 0; | ||
if (!this.hasHeader("Content-Type")) { | ||
this.contentType = "text"; | ||
} | ||
} | ||
} else if (this.contentType === "application/json") { | ||
if (!this.hasHeader("Content-Length")) { | ||
this.contentLength = Buffer.byteLength(String(null)); | ||
} | ||
}; | ||
// src/http/app.ts | ||
import { | ||
compose, | ||
flattenMiddlewareToken, | ||
i18n as i18n3 | ||
} from "@aomex/core"; | ||
// src/http/context.ts | ||
import createHttpError2 from "http-errors"; | ||
var WebContext = class { | ||
constructor(app, request, response2) { | ||
this.app = app; | ||
this.request = request; | ||
this.response = response2; | ||
request.app = response2.app = app; | ||
request.res = response2.res = response2; | ||
request.req = response2.req = request; | ||
request.ctx = response2.ctx = this; | ||
} | ||
send(statusOrBody, body2) { | ||
if (typeof statusOrBody === "number") { | ||
this.response.statusCode = statusOrBody; | ||
} else { | ||
body2 = statusOrBody; | ||
} | ||
this._determineNullBody = false; | ||
if (body2 !== void 0) { | ||
this.response.body = body2; | ||
} | ||
return this; | ||
} | ||
throw(arg, ...args) { | ||
throw createHttpError2(arg, ...args); | ||
} | ||
}; | ||
// src/http/app.ts | ||
import { EOL } from "node:os"; | ||
import { chalk } from "@aomex/internal-tools"; | ||
var WebApp = class extends EventEmitter { | ||
constructor(options = {}) { | ||
super(); | ||
this.options = options; | ||
this.middlewareList = []; | ||
if (options.locale) { | ||
i18n3.setLocale(options.locale); | ||
} | ||
if (options.mount) { | ||
this.point = options.mount["createPoint"](); | ||
this.middlewareList = flattenMiddlewareToken(options.mount); | ||
} | ||
} | ||
point; | ||
middlewareList; | ||
get debug() { | ||
return this.options.debug ?? process.env["NODE_ENV"] !== "production"; | ||
} | ||
callback() { | ||
const fn = compose(this.middlewareList); | ||
if (!this.listenerCount("error")) { | ||
this.on("error", this.log.bind(this)); | ||
} | ||
return (req, res) => { | ||
const ctx = new WebContext(this, req, res); | ||
return fn(ctx).then(res.flush).catch(res.onError); | ||
}; | ||
} | ||
listen = (...args) => { | ||
const server = createServer( | ||
{ | ||
IncomingMessage: WebRequest, | ||
ServerResponse: WebResponse | ||
}, | ||
this.callback() | ||
); | ||
return server.listen(...args); | ||
}; | ||
log(err, ctx) { | ||
if (ctx.response.statusCode === 404 || err.expose) | ||
return; | ||
const msgs = (err.stack || err.toString()).split(EOL, 2); | ||
console.error(["", chalk.bgRed(msgs.shift()), msgs.join(EOL), ""].join(EOL)); | ||
} | ||
}; | ||
// src/override/web-middleware.ts | ||
import { Middleware as Middleware2 } from "@aomex/core"; | ||
var WebMiddleware = class extends Middleware2 { | ||
openapiInjector; | ||
constructor(args) { | ||
const { fn, openapi = {} } = typeof args === "function" ? { fn: args } : args; | ||
super(fn); | ||
this.openapiInjector = openapi; | ||
} | ||
openapi() { | ||
return this.openapiInjector; | ||
} | ||
}; | ||
Middleware2.register("web", WebMiddleware); | ||
// src/override/web-middleware-chain.ts | ||
import { MiddlewareChain } from "@aomex/core"; | ||
var WebMiddlewareChain = class extends MiddlewareChain { | ||
}; | ||
MiddlewareChain.register("web", WebMiddlewareChain); | ||
// src/override/file.validator.ts | ||
import { PersistentFile } from "formidable"; | ||
import mimeTypes2 from "mime-types"; | ||
import { | ||
Validator, | ||
magistrate, | ||
Rule, | ||
i18n as i18n4 | ||
} from "@aomex/core"; | ||
import { bytes } from "@aomex/internal-tools"; | ||
import typeIs3 from "type-is"; | ||
var FileValidator = class extends Validator { | ||
/** | ||
* 允许的最大体积 | ||
* | ||
* 可选格式: | ||
* - 1024 | ||
* - 2048 | ||
* - '15KB' | ||
* - '20MB' | ||
*/ | ||
maxSize(byte) { | ||
const validator = this.copy(); | ||
validator.config.maxSize = typeof byte === "number" ? byte : bytes(byte); | ||
return validator; | ||
} | ||
mimeTypes(mineOrExt, ...others) { | ||
const validator = this.copy(); | ||
validator.config.mimeTypes = [ | ||
...new Set( | ||
[].concat(mineOrExt).concat(others).map(mimeTypes2.contentType).filter(Boolean) | ||
) | ||
]; | ||
return validator; | ||
} | ||
isEmpty(value) { | ||
return super.isEmpty(value) || Array.isArray(value) && !value.length; | ||
} | ||
validateValue(file, _key, label) { | ||
const { maxSize, mimeTypes: mimeTypes3 } = this.config; | ||
if (Array.isArray(file)) { | ||
file = file[0]; | ||
} | ||
if (!(file instanceof PersistentFile)) { | ||
return magistrate.fail(i18n4.t("web.validator.file.must_be_file", { label })); | ||
} | ||
if (maxSize !== void 0 && file.size > maxSize) { | ||
return magistrate.fail(i18n4.t("web.validator.file.too_large", { label })); | ||
} | ||
const hasMimeTypeLimitation = mimeTypes3 && mimeTypes3.length; | ||
if (hasMimeTypeLimitation && (!file.mimetype || !typeIs3.is(file.mimetype, ...mimeTypes3))) { | ||
return magistrate.fail( | ||
i18n4.t("web.validator.file.unsupported_mimetype", { label }) | ||
); | ||
} | ||
return magistrate.ok(file); | ||
} | ||
toDocument() { | ||
return { | ||
type: "string", | ||
format: "binary" | ||
}; | ||
} | ||
}; | ||
Rule.register("file", FileValidator); | ||
// src/middleware/body.ts | ||
import { validate, Validator as Validator2, rule, ValidatorError } from "@aomex/core"; | ||
var WebBodyMiddleware = class extends WebMiddleware { | ||
constructor(props) { | ||
super(async (ctx, next) => { | ||
import { validate, Validator as Validator2, rule, ValidatorError, middleware } from "@aomex/core"; | ||
var body = (fields) => { | ||
return middleware.web({ | ||
fn: async (ctx, next) => { | ||
try { | ||
ctx.body = await validate(ctx.request.body, props, { | ||
throwIfError: true | ||
}); | ||
ctx.body = await validate(ctx.request.body, fields); | ||
return next(); | ||
@@ -684,31 +618,27 @@ } catch (e) { | ||
} | ||
}); | ||
this.props = props; | ||
} | ||
toDocument(options) { | ||
if (!options.methodItem) | ||
return; | ||
options.methodItem.requestBody = { | ||
content: { | ||
"*/*": { | ||
schema: Validator2.toDocument(rule.object(this.props)).schema | ||
} | ||
}, | ||
required: Object.values(this.props).some( | ||
(validator) => Validator2.toDocument(validator).required | ||
) | ||
}; | ||
} | ||
}, | ||
openapi: { | ||
onMethod(methodItem) { | ||
methodItem.requestBody = { | ||
content: { | ||
"*/*": { | ||
schema: Validator2.toDocument(rule.object(fields)).schema | ||
} | ||
}, | ||
required: Object.values(fields).some( | ||
(validator) => Validator2.toDocument(validator).required | ||
) | ||
}; | ||
} | ||
} | ||
}); | ||
}; | ||
var body = (fields) => new WebBodyMiddleware(fields); | ||
// src/middleware/query.ts | ||
import { validate as validate2, Validator as Validator3, ValidatorError as ValidatorError2 } from "@aomex/core"; | ||
var WebQueryMiddleware = class extends WebMiddleware { | ||
constructor(props) { | ||
super(async (ctx, next) => { | ||
import { middleware as middleware2, validate as validate2, Validator as Validator3, ValidatorError as ValidatorError2 } from "@aomex/core"; | ||
var query = (fields) => { | ||
return middleware2.web({ | ||
fn: async (ctx, next) => { | ||
try { | ||
ctx.query = await validate2(ctx.request.query, props, { | ||
throwIfError: true | ||
}); | ||
ctx.query = await validate2(ctx.request.query, fields); | ||
return next(); | ||
@@ -718,30 +648,25 @@ } catch (e) { | ||
} | ||
}); | ||
this.props = props; | ||
} | ||
toDocument(options) { | ||
const methodItem = options.methodItem; | ||
if (!methodItem) | ||
return; | ||
methodItem.parameters ||= []; | ||
Object.entries(this.props).forEach(([name, validator]) => { | ||
methodItem.parameters.push({ | ||
name, | ||
in: "query", | ||
...Validator3.toDocument(validator) | ||
}); | ||
}); | ||
} | ||
}, | ||
openapi: { | ||
onMethod(methodItem) { | ||
methodItem.parameters ||= []; | ||
Object.entries(fields).forEach(([name, validator]) => { | ||
methodItem.parameters.push({ | ||
name, | ||
in: "query", | ||
...Validator3.toDocument(validator) | ||
}); | ||
}); | ||
} | ||
} | ||
}); | ||
}; | ||
var query = (fields) => new WebQueryMiddleware(fields); | ||
// src/middleware/params.ts | ||
import { validate as validate3, Validator as Validator4, ValidatorError as ValidatorError3 } from "@aomex/core"; | ||
var WebParamMiddleware = class extends WebMiddleware { | ||
constructor(props) { | ||
super(async (ctx, next) => { | ||
import { middleware as middleware3, validate as validate3, Validator as Validator4, ValidatorError as ValidatorError3 } from "@aomex/core"; | ||
var params = (fields) => { | ||
return middleware3.web({ | ||
fn: async (ctx, next) => { | ||
try { | ||
ctx.params = await validate3(ctx.request.params, props, { | ||
throwIfError: true | ||
}); | ||
ctx.params = await validate3(ctx.request.params, fields); | ||
return next(); | ||
@@ -751,108 +676,89 @@ } catch (e) { | ||
} | ||
}); | ||
this.props = props; | ||
} | ||
toDocument(options) { | ||
const methodItem = options.methodItem; | ||
if (!methodItem) | ||
return; | ||
methodItem.parameters ||= []; | ||
Object.entries(this.props).forEach(([name, validator]) => { | ||
const validatorDocument = Validator4.toDocument(validator); | ||
methodItem.parameters.push({ | ||
name, | ||
in: "path", | ||
...validatorDocument, | ||
// path parameter must have "required" property that is set to "true" | ||
required: validatorDocument.required === true | ||
}); | ||
}); | ||
} | ||
}, | ||
openapi: { | ||
onMethod(methodItem) { | ||
methodItem.parameters ||= []; | ||
Object.entries(fields).forEach(([name, validator]) => { | ||
const validatorDocument = Validator4.toDocument(validator); | ||
methodItem.parameters.push({ | ||
name, | ||
in: "path", | ||
...validatorDocument, | ||
// path必填参数 | ||
required: validatorDocument.required === true | ||
}); | ||
}); | ||
} | ||
} | ||
}); | ||
}; | ||
var params = (fields) => new WebParamMiddleware(fields); | ||
// src/middleware/response.ts | ||
import { | ||
forceToValidator, | ||
validate as validate4, | ||
Validator as Validator5 | ||
} from "@aomex/core"; | ||
import { Stream as Stream2 } from "node:stream"; | ||
var num3length = /^\d{3}$/; | ||
var num2length = /^\d{2}x$/i; | ||
var num1length = /^\dxx$/i; | ||
var response = (options) => new WebResponseMiddleware(options); | ||
import { toValidator, Validator as Validator5 } from "@aomex/core"; | ||
var WebResponseMiddleware = class extends WebMiddleware { | ||
constructor(options) { | ||
const schema = forceToValidator(options.schema)?.strict(); | ||
const headerSchema = forceToValidator(options.headers)?.strict(); | ||
super(async (ctx, next) => { | ||
await next(); | ||
const { statusCode, body: body2 } = ctx.response; | ||
const shouldValidate = options.validate ?? ctx.app.options.validateResponse ?? ctx.app.debug; | ||
if (!shouldValidate) | ||
return; | ||
if (!this.matchStatusCode(options.statusCode, statusCode)) | ||
return; | ||
try { | ||
if (schema && !(body2 instanceof Stream2)) { | ||
await validate4(body2, schema); | ||
super(async (_, next) => next()); | ||
this.options = options; | ||
} | ||
openapi() { | ||
return { | ||
onMethod: (methodItem) => { | ||
methodItem.responses ||= {}; | ||
const { statusCode, schema, headers, example, description = "" } = this.options; | ||
const resItem = methodItem.responses[statusCode] = { | ||
description | ||
}; | ||
if (schema) { | ||
resItem.content = { | ||
[this.getContentType()]: { | ||
schema: Validator5.toDocument(toValidator(schema)).schema, | ||
example | ||
} | ||
}; | ||
} | ||
if (headerSchema) { | ||
await validate4(ctx.response.getHeaders(), headerSchema); | ||
if (headers) { | ||
resItem.headers = Object.fromEntries( | ||
Object.entries(headers).map(([key, header]) => [ | ||
key, | ||
Validator5.toDocument(header) | ||
]) | ||
); | ||
} | ||
} catch (e) { | ||
ctx.throw(500, e); | ||
} | ||
}); | ||
this.options = options; | ||
}; | ||
} | ||
matchStatusCode(expected, statusCode) { | ||
expected = expected.toString(); | ||
if (num3length.test(expected)) { | ||
return statusCode === Number(expected); | ||
getContentType() { | ||
let { contentType: contentType3, schema } = this.options; | ||
if (!contentType3) { | ||
const validator = toValidator(schema); | ||
const docs = validator["toDocument"](); | ||
switch (docs.type) { | ||
case "array": | ||
case "object": | ||
contentType3 = getMimeType("json"); | ||
break; | ||
case "boolean": | ||
case "integer": | ||
case "number": | ||
contentType3 = getMimeType("text"); | ||
break; | ||
case "string": | ||
if (docs.format === "binary") { | ||
contentType3 = getMimeType("bin"); | ||
} else { | ||
contentType3 = getMimeType("text"); | ||
} | ||
break; | ||
default: | ||
contentType3 = "*/*"; | ||
} | ||
} else if (!contentType3.includes("*")) { | ||
contentType3 = getMimeType(contentType3); | ||
} | ||
if (num1length.test(expected)) { | ||
return expected[0] === statusCode.toString()[0]; | ||
} | ||
if (num2length.test(expected)) { | ||
return expected.slice(0, 2) === statusCode.toString().slice(0, 2); | ||
} | ||
return false; | ||
return contentType3.split(";")[0]; | ||
} | ||
toDocument({ methodItem }) { | ||
if (!methodItem) | ||
return; | ||
methodItem.responses ||= {}; | ||
const { | ||
statusCode, | ||
contentType: contentType3 = "*/*", | ||
schema, | ||
headers, | ||
example, | ||
description = "" | ||
} = this.options; | ||
const responseObject = methodItem.responses[statusCode] = { description }; | ||
if (schema) { | ||
responseObject.content = { | ||
[this.fixContentType(contentType3)]: { | ||
schema: Validator5.toDocument(forceToValidator(schema)).schema, | ||
example | ||
} | ||
}; | ||
} | ||
if (headers) { | ||
responseObject.headers = Object.fromEntries( | ||
Object.entries(headers).map(([key, header]) => [ | ||
key, | ||
Validator5.toDocument(header) | ||
]) | ||
); | ||
} | ||
} | ||
fixContentType(contentType3) { | ||
const type = contentType3.includes("*") ? contentType3 : getMimeType(contentType3) || "*/*"; | ||
return type.split(";", 1)[0]; | ||
} | ||
}; | ||
var response = (options) => { | ||
return new WebResponseMiddleware(options); | ||
}; | ||
@@ -864,10 +770,6 @@ // src/index.ts | ||
FileValidator, | ||
METHOD, | ||
WebApp, | ||
WebBodyMiddleware, | ||
WebChain, | ||
WebContext, | ||
WebMiddleware, | ||
WebParamMiddleware, | ||
WebQueryMiddleware, | ||
WebMiddlewareChain, | ||
WebRequest, | ||
@@ -878,10 +780,7 @@ WebResponse, | ||
default2 as createHttpError, | ||
createHttpServer, | ||
getMimeType, | ||
params, | ||
query, | ||
response, | ||
skip, | ||
default3 as statuses | ||
}; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@aomex/web", | ||
"version": "0.0.29", | ||
"description": "", | ||
"version": "1.0.0", | ||
"description": "aomex web层应用", | ||
"type": "module", | ||
@@ -14,5 +14,2 @@ "types": "dist/index.d.ts", | ||
}, | ||
"bin": { | ||
"aomex-ts-node": "dist/bin.js" | ||
}, | ||
"publishConfig": { | ||
@@ -33,3 +30,3 @@ "access": "public" | ||
"peerDependencies": { | ||
"@aomex/core": "^0.0.28" | ||
"@aomex/core": "^1.0.0" | ||
}, | ||
@@ -42,4 +39,4 @@ "dependencies": { | ||
"@types/http-errors": "^2.0.4", | ||
"@types/qs": "^6.9.10", | ||
"@types/statuses": "^2.0.4", | ||
"@types/qs": "^6.9.14", | ||
"@types/statuses": "^2.0.5", | ||
"accepts": "^1.3.8", | ||
@@ -51,3 +48,3 @@ "co-body": "^6.1.0", | ||
"destroy": "^1.2.0", | ||
"encodeurl": "^1.0.2", | ||
"encodeurl": "^2.0.0", | ||
"escape-html": "^1.0.3", | ||
@@ -57,14 +54,13 @@ "formidable": "^3.5.1", | ||
"http-errors": "^2.0.0", | ||
"lru-cache": "^10.1.0", | ||
"lru-cache": "^10.2.0", | ||
"mime-types": "^2.1.35", | ||
"qs": "^6.11.2", | ||
"qs": "^6.12.0", | ||
"request-ip": "^3.3.0", | ||
"statuses": "^2.0.1", | ||
"ts-node": "^10.9.2", | ||
"type-is": "^1.6.18", | ||
"vary": "^1.1.2", | ||
"@aomex/internal-tools": "^0.0.27" | ||
"@aomex/internal-tools": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@aomex/core": "^0.0.28", | ||
"@aomex/core": "^1.0.0", | ||
"@types/co-body": "^6.1.3", | ||
@@ -81,5 +77,3 @@ "@types/content-type": "^1.1.8", | ||
}, | ||
"scripts": { | ||
"test": "vitest" | ||
} | ||
"scripts": {} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
27
0
89482
6
1166
+ Added@aomex/core@1.7.0(transitive)
+ Added@aomex/internal-tools@1.7.0(transitive)
+ Addedencodeurl@2.0.0(transitive)
- Removedts-node@^10.9.2
- Removed@aomex/core@0.0.28(transitive)
- Removed@aomex/internal-tools@0.0.27(transitive)
- Removed@cspotcode/source-map-support@0.8.1(transitive)
- Removed@jridgewell/resolve-uri@3.1.2(transitive)
- Removed@jridgewell/sourcemap-codec@1.5.0(transitive)
- Removed@jridgewell/trace-mapping@0.3.9(transitive)
- Removed@tsconfig/node10@1.0.11(transitive)
- Removed@tsconfig/node12@1.0.11(transitive)
- Removed@tsconfig/node14@1.0.3(transitive)
- Removed@tsconfig/node16@1.0.4(transitive)
- Removedacorn@8.12.1(transitive)
- Removedacorn-walk@8.3.4(transitive)
- Removedarg@4.1.3(transitive)
- Removedchalk@5.3.0(transitive)
- Removedcreate-require@1.1.1(transitive)
- Removeddiff@4.0.2(transitive)
- Removedencodeurl@1.0.2(transitive)
- Removedip-regex@5.0.0(transitive)
- Removedmake-error@1.3.6(transitive)
- Removedts-node@10.9.2(transitive)
- Removedv8-compile-cache-lib@3.0.1(transitive)
- Removedyn@3.1.1(transitive)
Updated@aomex/internal-tools@^1.0.0
Updated@types/qs@^6.9.14
Updated@types/statuses@^2.0.5
Updatedencodeurl@^2.0.0
Updatedlru-cache@^10.2.0
Updatedqs@^6.12.0