moleculer-web
Version:
Official API Gateway service for Moleculer framework
727 lines (661 loc) • 21.9 kB
TypeScript
import { IParseOptions } from "qs";
import bodyParser from "body-parser";
import type serveStatic, { ServeStaticOptions } from "serve-static";
import type {
ActionEndpoint,
ActionSchema,
CallingOptions,
Context,
LogLevels,
Service,
ServiceBroker,
ServiceSchema,
ServiceSettingSchema
} from "moleculer";
import { Errors } from "moleculer";
interface RestSchema {
path?: string;
method?: "GET" | "POST" | "DELETE" | "PUT" | "PATCH";
fullPath?: string;
basePath?: string;
}
import "moleculer";
declare module "moleculer" {
interface ActionSchema {
rest?: RestSchema | RestSchema[] | string | string[] | null;
}
interface ServiceSettingSchema {
rest?: string | string[] | null;
}
}
import { IncomingMessage, ServerResponse } from "http";
import type { Server as NetServer } from 'net';
import type { Server as TLSServer } from 'tls';
import type { Server as HttpServer } from 'http';
import type { Server as HttpsServer } from 'https';
import type { Http2SecureServer, Http2Server } from 'http2';
// RateLimit
export type generateRateLimitKey = (req: IncomingMessage) => string;
export interface RateLimitSettings {
/**
* How long to keep record of requests in memory (in milliseconds).
* @default 60000 (1 min)
*/
window?: number;
/**
* Max number of requests during window.
* @default 30
*/
limit?: number;
/**
* Set rate limit headers to response.
* @default false
*/
headers?: boolean;
/**
* Function used to generate keys.
* @default req => req.headers["x-forwarded-for"] || req.connection.remoteAddress || req.socket.remoteAddress || req.connection.socket.remoteAddress
*/
key?: generateRateLimitKey;
/**
* use rate limit Custom Store
* @default MemoryStore
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Custom-Store-example
*/
StoreFactory?: typeof RateLimitStore;
}
export abstract class RateLimitStore {
resetTime: number;
constructor(clearPeriod: number, opts?: RateLimitSettings, broker?: ServiceBroker);
inc(key: string): number | Promise<number>;
}
export interface RateLimitStores {
MemoryStore: typeof MemoryStore;
}
class MemoryStore extends RateLimitStore {
constructor(clearPeriod: number, opts?: RateLimitSettings, broker?: ServiceBroker);
/**
* Increment the counter by key
*/
inc(key: string): number;
/**
* Reset all counters
*/
reset(): void;
}
// bodyParserOptions
/**
* DefinitelyTyped body-parser
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/body-parser/index.d.ts#L24
*/
namespace BodyParser {
interface Options {
/** When set to true, then deflated (compressed) bodies will be inflated; when false, deflated bodies are rejected. Defaults to true. */
inflate?: boolean | undefined;
/**
* Controls the maximum request body size. If this is a number,
* then the value specifies the number of bytes; if it is a string,
* the value is passed to the bytes library for parsing. Defaults to '100kb'.
*/
limit?: number | string | undefined;
/**
* The type option is used to determine what media type the middleware will parse
*/
type?: string | string[] | ((req: IncomingMessage) => any) | undefined;
/**
* The verify option, if supplied, is called as verify(req, res, buf, encoding),
* where buf is a Buffer of the raw request body and encoding is the encoding of the request.
*/
verify?(req: IncomingMessage, res: ServerResponse, buf: Buffer, encoding: string): void;
}
interface OptionsJson extends Options {
/**
*
* The reviver option is passed directly to JSON.parse as the second argument.
*/
reviver?(key: string, value: any): any;
/**
* When set to `true`, will only accept arrays and objects;
* when `false` will accept anything JSON.parse accepts. Defaults to `true`.
*/
strict?: boolean | undefined;
}
interface OptionsText extends Options {
/**
* Specify the default character set for the text content if the charset
* is not specified in the Content-Type header of the request.
* Defaults to `utf-8`.
*/
defaultCharset?: string | undefined;
}
interface OptionsUrlencoded extends Options {
/**
* The extended option allows to choose between parsing the URL-encoded data
* with the querystring library (when `false`) or the qs library (when `true`).
*/
extended?: boolean | undefined;
/**
* The parameterLimit option controls the maximum number of parameters
* that are allowed in the URL-encoded data. If a request contains more parameters than this value,
* a 413 will be returned to the client. Defaults to 1000.
*/
parameterLimit?: number | undefined;
}
}
type bodyParserOptions = {
json?: BodyParser.OptionsJson | boolean;
urlencoded?: BodyParser.OptionsUrlencoded | boolean;
text?: BodyParser.OptionsText | boolean;
raw?: BodyParser.Options | boolean;
};
// BusboyConfig
namespace busboy {
interface BusboyConfig {
headers?: any;
highWaterMark?: number | undefined;
fileHwm?: number | undefined;
defCharset?: string | undefined;
preservePath?: boolean | undefined;
limits?:
| {
fieldNameSize?: number | undefined;
fieldSize?: number | undefined;
fields?: number | undefined;
fileSize?: number | undefined;
files?: number | undefined;
parts?: number | undefined;
headerPairs?: number | undefined;
}
| undefined;
}
interface Busboy extends NodeJS.WritableStream {
on(
event: "field",
listener: (
fieldname: string,
val: any,
fieldnameTruncated: boolean,
valTruncated: boolean,
encoding: string,
mimetype: string,
) => void,
): this;
on(
event: "file",
listener: (
fieldname: string,
file: NodeJS.ReadableStream,
filename: string,
encoding: string,
mimetype: string,
) => void,
): this;
on(event: "finish", callback: () => void): this;
on(event: "partsLimit", callback: () => void): this;
on(event: "filesLimit", callback: () => void): this;
on(event: "fieldsLimit", callback: () => void): this;
on(event: string, listener: Function): this;
}
}
type onEventBusboyConfig<T> = (busboy: busboy.Busboy, alias: T, service: Service) => void;
type BusboyConfig<T> = busboy.BusboyConfig & {
onFieldsLimit?: T;
onFilesLimit?: T;
onPartsLimit?: T;
};
export type AssetsConfig = {
/**
* Root folder of assets
*/
folder: string;
/**
* Further options to `server-static` module
*/
options?: ServeStaticOptions;
};
export interface ContextResponseMeta {
$responseType?: string;
$statusCode?: number;
$statusMessage?: string;
$location?: string;
$responseHeaders?: Record<string, string>;
}
// CorsOptions
// From: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/cors/index.d.ts
type CustomOrigin = (origin: string) => boolean;
export interface CorsOptions {
origin?: boolean | string | RegExp | (string | RegExp)[] | CustomOrigin;
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
}
class InvalidRequestBodyError extends Errors.MoleculerError {
constructor(body: any, error: any);
}
class InvalidResponseTypeError extends Errors.MoleculerError {
constructor(dataType: string);
}
class UnAuthorizedError extends Errors.MoleculerError {
constructor(type: string | null | undefined, data?: any);
}
class ForbiddenError extends Errors.MoleculerError {
constructor(type: string, data?: any);
}
class BadRequestError extends Errors.MoleculerError {
constructor(type: string, data?: any);
}
class RateLimitExceeded extends Errors.MoleculerClientError {
constructor(type: string, data?: any);
}
class NotFoundError extends Errors.MoleculerClientError {
constructor(type: string, data?: any);
}
class ServiceUnavailableError extends Errors.MoleculerError {
constructor(type: string, data?: any);
}
export interface ApiGatewayErrors {
InvalidRequestBodyError: typeof InvalidRequestBodyError;
InvalidResponseTypeError: typeof InvalidResponseTypeError;
UnAuthorizedError: typeof UnAuthorizedError;
ForbiddenError: typeof ForbiddenError;
BadRequestError: typeof BadRequestError;
RateLimitExceeded: typeof RateLimitExceeded;
NotFoundError: typeof NotFoundError;
ServiceUnavailableError: typeof ServiceUnavailableError;
ERR_NO_TOKEN: "ERR_NO_TOKEN";
ERR_INVALID_TOKEN: "ERR_INVALID_TOKEN";
ERR_UNABLE_DECODE_PARAM: "ERR_UNABLE_DECODE_PARAM";
ERR_ORIGIN_NOT_FOUND: "ORIGIN_NOT_FOUND";
}
export class Alias {
_generated: boolean;
service: Service;
route: Route;
type: string;
method: string;
path: string;
handler: null | Function[];
action: string;
}
export class Route {
callOptions: any;
cors: CorsOptions;
etag: boolean | "weak" | "strong" | Function;
hasWhitelist: boolean;
hasBlacklist: boolean;
logging: boolean;
mappingPolicy: string;
middlewares: Function[];
onBeforeCall?: onBeforeCall;
onAfterCall?: onAfterCall;
opts: any;
path: string;
whitelist: string[];
blacklist: string[];
}
type onBeforeCall = (
ctx: Context,
route: Route,
req: IncomingRequest,
res: GatewayResponse,
) => void;
type onAfterCall = (
ctx: Context,
route: Route,
req: IncomingRequest,
res: GatewayResponse,
data: any,
) => any;
/**
* Expressjs next function<br>
* /@types/express-serve-static-core/index.d.ts:36
* @see https://www.npmjs.com/package/@types/express-serve-static-core
*/
interface NextFunction {
(err?: any): void;
/**
* "Break-out" of a router by calling {next('router')};
* @see https://expressjs.com/en/guide/using-middleware.html#middleware.router
*/
(deferToNext: "router"): void;
/**
* "Break-out" of a route by calling {next('route')};
* @see https://expressjs.com/en/guide/using-middleware.html#middleware.application
*/
(deferToNext: "route"): void;
}
type routeMiddleware = (req: IncomingRequest, res: GatewayResponse, next: NextFunction) => void;
type routeMiddlewareError = (
err: any,
req: IncomingRequest,
res: GatewayResponse,
next: NextFunction,
) => void;
type ETagFunction = (body: any) => string;
type AliasFunction = (
req: IncomingRequest,
res: GatewayResponse,
next?: (err?: any) => void,
) => void;
type AliasRouteSchema = {
type?: "call" | "multipart" | "stream" | string;
method?: "GET" | "POST" | "PUT" | "DELETE" | "*" | "HEAD" | "OPTIONS" | "PATCH" | string;
path?: string;
handler?: AliasFunction;
action?: string;
busboyConfig?: BusboyConfig<onEventBusboyConfig<Alias>>;
[k: string]: any;
};
export interface CommonSettingSchema {
/**
* Cross-origin resource sharing configuration (using module [cors](https://www.npmjs.com/package/cors))<br>
* @example {
// Configures the Access-Control-Allow-Origin CORS header.
origin: "*", // ["http://localhost:3000", "https://localhost:4000"],
// Configures the Access-Control-Allow-Methods CORS header.
methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
// Configures the Access-Control-Allow-Headers CORS header.
allowedHeaders: [],
// Configures the Access-Control-Expose-Headers CORS header.
exposedHeaders: [],
// Configures the Access-Control-Allow-Credentials CORS header.
credentials: false,
// Configures the Access-Control-Max-Age CORS header.
maxAge: 3600
}
* @see https://moleculer.services/docs/0.14/moleculer-web.html#CORS-headers
*/
cors?: boolean | CorsOptions;
/**
* The etag option value can be `false`, `true`, `weak`, `strong`, or a custom `Function`
* @default settings.etag (null)
* @see https://moleculer.services/docs/0.14/moleculer-web.html#ETag
*/
etag?: boolean | "weak" | "strong" | ETagFunction;
/**
* You can add route-level & global-level custom error handlers.<br>
* In handlers, you must call the `res.end`. Otherwise, the request is unhandled.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Error-handlers
*/
onError?: (req: IncomingRequest, res: ServerResponse, error: Error) => void;
/**
* The Moleculer-Web has a built-in rate limiter with a memory store.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Rate-limiter
*/
rateLimit?: RateLimitSettings;
/**
* It supports Connect-like middlewares in global-level, route-level & alias-level.<br>
* Signature: function (req, res, next) {...}.<br>
* Signature: function (err, req, res, next) {...}.<br>
* For more info check [express middleware](https://expressjs.com/en/guide/using-middleware.html)
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares
*/
use?: (routeMiddleware | routeMiddlewareError)[];
}
export interface ApiRouteSchema extends CommonSettingSchema {
/**
* You can use alias names instead of action names. You can also specify the method. Otherwise it will handle every method types.<br>
* Using named parameters in aliases is possible. Named parameters are defined by prefixing a colon to the parameter name (:name).
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Aliases
*/
aliases?: {
[k: string]: string | AliasFunction | (AliasFunction | string)[] | AliasRouteSchema;
};
/**
* To enable the support for authentication, you need to do something similar to what is describe in the Authorization paragraph.<br>
* Also in this case you have to:
* 1. Set `authentication: true` in your routes
* 2. Define your custom authenticate method in your service
* 3. The returned value will be set to the `ctx.meta.user` property. You can use it in your actions to get the logged in user entity.
* <br>`From v0.10.3`: You can define custom `authentication` and `authorization` methods for every routes.
* In this case you should set `the method name` instead of `true` value.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Authentication
*/
authentication?: boolean | string;
/**
* You can implement authorization. Do 2 things to enable it.
* 1. Set authorization: true in your routes.
* 2. Define the authorize method in service.
* <br>`From v0.10.3`: You can define custom `authentication` and `authorization` methods for every routes.
* In this case you should set `the method name` instead of `true` value.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Authorization
*/
authorization?: boolean | string;
/**
* The auto-alias feature allows you to declare your route alias directly in your services.<br>
* The gateway will dynamically build the full routes from service schema.
* Gateway will regenerate the routes every time a service joins or leaves the network.<br>
* Use `whitelist` parameter to specify services that the Gateway should track and build the routes.
* And `blacklist` parameter to specify services that the Gateway should not track and build the routes.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Auto-alias
*/
autoAliases?: boolean;
/**
* Parse incoming request bodies, available under the `ctx.params` property
* @see https://www.npmjs.com/package/body-parser
*/
bodyParsers?: bodyParserOptions | boolean;
/**
* API Gateway has implemented file uploads.<br>
* You can upload files as a multipart form data (thanks to [busboy](https://github.com/mscdex/busboy) library) or as a raw request body.<br>
* In both cases, the file is transferred to an action as a Stream.<br>
* In multipart form data mode you can upload multiple files, as well.<br>
* `Please note`: you have to disable other body parsers in order to accept files.
*/
busboyConfig?: BusboyConfig<onEventBusboyConfig<Alias>>;
/**
* The route has a callOptions property which is passed to broker.call. So you can set timeout, retries or fallbackResponse options for routes.
* @see https://moleculer.services/docs/0.14/actions.html#Call-services
*/
callOptions?: CallingOptions;
/**
* If alias handler not found, `api` will try to call service by action name<br>
* This option will convert request url to camelCase before call action
* @example `/math/sum-all` => `math.sumAll`
* @default: null
*/
camelCaseNames?: boolean;
/**
* Debounce wait time before call to regenerated aliases when got event "$services.changed"
* @default 500
*/
debounceTime?: number;
/**
* Enable/disable logging
* @default true
*/
logging?: boolean;
/**
* The route has a `mappingPolicy` property to handle routes without aliases.<br>
* Available options:<br>
* `all` - enable to request all routes with or without aliases (default)<br>
* `restrict` - enable to request only the routes with aliases.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy
*/
mappingPolicy?: "all" | "restrict";
/**
* To disable parameter merging set `mergeParams: false` in route settings.<br>
* Default is `true`
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Disable-merging
*/
mergeParams?: boolean;
/**
* `From v0.10.2`
* <br>Support multiple routes with the same path.
* <br>You should give a unique name for the routes if they have same path.
* @see https://github.com/moleculerjs/moleculer-web/releases/tag/v0.10.2
*/
name?: string;
/**
* The route has before & after call hooks. You can use it to set `ctx.meta`, access `req.headers` or modify the response data.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Route-hooks
*/
onBeforeCall?: onBeforeCall;
/**
* You could manipulate the data in `onAfterCall`.<br>
* `Must always return the new or original data`.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Route-hooks
*/
onAfterCall?: onAfterCall;
/**
* Path prefix to this route
*/
path: string;
/**
* If you don’t want to publish all actions, you can filter them with whitelist option.<br>
* Use match strings or regexp in list. To enable all actions, use "**" item.<br>
* "posts.*": `Access any actions in 'posts' service`<br>
* "users.list": `Access call only the 'users.list' action`<br>
* /^math\.\w+$/: `Access any actions in 'math' service`<br>
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Whitelist
*/
whitelist?: (string | RegExp)[];
/**
* If you don’t want to publish all actions, you can filter them with blacklist option.<br>
* Use match strings or regexp in list. To enable all actions, use "**" item.<br>
* "posts.*": `Access any actions in 'posts' service`<br>
* "users.list": `Access call only the 'users.list' action`<br>
* /^math\.\w+$/: `Access any actions in 'math' service`<br>
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Blacklist
*/
blacklist?: (string | RegExp)[];
}
type APISettingServer =
| boolean
| HttpServer
| HttpsServer
| Http2Server
| Http2SecureServer
| NetServer
| TLSServer;
export interface ApiSettingsSchema extends ServiceSettingSchema, CommonSettingSchema {
/**
* It serves assets with the [serve-static](https://github.com/expressjs/serve-static) module like ExpressJS.
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Serve-static-files
*/
assets?: AssetsConfig;
/**
* Use HTTP2 server (experimental)
* @default false
*/
http2?: boolean;
/**
* HTTP Server Timeout
* @default null
*/
httpServerTimeout?: number;
/**
* Special char for internal services<br>
* Note: `RegExp` type is not official
* @default "~"
* @example "~" => /~node/~action => /$node/~action
* @example /[0-9]+/g => /01234demo/hello2021 => /demo/hello `(not official)`
*/
internalServiceSpecialChar?: string | RegExp;
/**
* Exposed IP
* @default process.env.IP || "0.0.0.0"
*/
ip?: string;
/**
* If set to true, it will log 4xx client errors, as well
* @default false
*/
log4XXResponses?: boolean;
/**
* Log each request (default to "info" level)
* @default "info"
*/
logRequest?: LogLevels | null;
/**
* Log the request ctx.params (default to "debug" level)
* @default "debug"
*/
logRequestParams?: LogLevels | null;
/**
* Log each response (default to "info" level)
* @default "info"
*/
logResponse?: LogLevels | null;
/**
* Log the response data (default to disable)
* @default null
*/
logResponseData?: LogLevels | null;
/**
* Log the route registration/aliases related activity
* @default "info"
*/
logRouteRegistration?: LogLevels | null;
/**
* Optimize route order
* @default true
*/
optimizeOrder?: boolean;
/**
* Global path prefix
*/
path?: string;
/**
* Exposed port
* @default process.env.PORT || 3000
*/
port?: number;
/**
* Gateway routes
* @default []
*/
routes?: ApiRouteSchema[];
/**
* CallOption for the root action `api.rest`
* @default null
*/
rootCallOptions?: CallingOptions;
/**
* Used server instance. If null, it will create a new HTTP(s)(2) server<br>
* If false, it will start without server in middleware mode
* @default true
*/
server?: APISettingServer;
/**
* Options passed on to qs
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Query-string-parameters
*/
qsOptions?: IParseOptions;
/**
* for extra setting's keys
*/
[k: string]: any;
}
export class IncomingRequest extends IncomingMessage {
$action: ActionSchema;
$alias: Alias;
$ctx: Context<{ req: IncomingMessage; res: ServerResponse; }>;
$endpoint: ActionEndpoint;
$next: any;
$params: any;
$route: Route;
$service: Service;
$startTime: number[];
originalUrl: string;
parsedUrl: string;
query: Record<string, string>;
}
export class GatewayResponse extends ServerResponse {
$ctx: Context;
$route: Route;
$service: Service;
locals: Record<string, unknown>;
}
const ApiGatewayService: ServiceSchema & {
Errors: ApiGatewayErrors;
RateLimitStores: RateLimitStores;
bodyParser: bodyParser;
serveStatic: serveStatic;
};
export default ApiGatewayService;
export = ApiGatewayService;