Socket
Socket
Sign inDemoInstall

@aomex/web

Package Overview
Dependencies
Maintainers
1
Versions
57
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@aomex/web - npm Package Compare versions

Comparing version 0.0.29 to 1.0.0

74

CHANGELOG.md
# @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

497

dist/index.d.ts

@@ -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 };

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc