UNPKG

moleculer-web

Version:

Official API Gateway service for Moleculer framework

727 lines (661 loc) 21.9 kB
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;