UNPKG

moleculer-web

Version:

Official API Gateway service for Moleculer framework

236 lines (200 loc) 6.19 kB
/* * moleculer * Copyright (c) 2021 MoleculerJS (https://github.com/moleculerjs/moleculer) * MIT Licensed */ "use strict"; const pathToRegexp = require("path-to-regexp"); const Busboy = require("@fastify/busboy"); const kleur = require("kleur"); const _ = require("lodash"); const { PayloadTooLarge } = require("./errors"); const { MoleculerClientError } = require("moleculer").Errors; const { removeTrailingSlashes, addSlashes, decodeParam, compose } = require("./utils"); class Alias { /** * Constructor of Alias * * @param {Service} service * @param {Object} route * @param {Object} opts * @param {any} action */ constructor(service, route, opts, action) { this.service = service; this.route = route; this.type = "call"; this.method = "*"; this.path = null; this.handler = null; this.action = null; if (_.isString(opts)) { // Parse alias string if (opts.indexOf(" ") !== -1) { const p = opts.split(/\s+/); this.method = p[0]; this.path = p[1]; } else { this.path = opts; } } else if (_.isObject(opts)) { Object.assign(this, _.cloneDeep(opts)); } if (_.isString(action)) { // Parse type from action name if (action.indexOf(":") > 0) { const p = action.split(":"); this.type = p[0]; this.action = p[1]; } else { this.action = action; } } else if (_.isFunction(action)) { this.handler = action; this.action = null; } else if (Array.isArray(action)) { const mws = _.compact(action.map(mw => { if (_.isString(mw)) this.action = mw; else if(_.isFunction(mw)) return mw; })); this.handler = compose.call(service, ...mws); } else if (action != null) { Object.assign(this, _.cloneDeep(action)); } this.type = this.type || "call"; this.path = removeTrailingSlashes(this.path); this.fullPath = this.fullPath || (addSlashes(this.route.path) + this.path); if (this.fullPath !== "/" && this.fullPath.endsWith("/")) { this.fullPath = this.fullPath.slice(0, -1); } this.keys = []; this.re = pathToRegexp(this.fullPath, this.keys, route.opts.pathToRegexpOptions || {}); // Options: https://github.com/pillarjs/path-to-regexp#usage if (this.type == "multipart") { // Handle file upload in multipart form this.handler = this.multipartHandler.bind(this); } } /** * * @param {*} url */ match(url) { const m = this.re.exec(url); if (!m) return false; const params = {}; let key, param; for (let i = 0; i < this.keys.length; i++) { key = this.keys[i]; param = m[i + 1]; if (!param) continue; params[key.name] = decodeParam(param); if (key.repeat) params[key.name] = params[key.name].split(key.delimiter); } return params; } /** * * @param {*} method */ isMethod(method) { return this.method === "*" || this.method === method; } /** * */ printPath() { /* istanbul ignore next */ return `${this.method} ${this.fullPath}`; } /** * */ toString() { return kleur.magenta(_.padStart(this.method, 6)) + " " + kleur.cyan(this.fullPath) + kleur.grey(" => ") + (this.handler != null && this.type !== "multipart" ? "<Function>" : this.action); } /** * * @param {*} req * @param {*} res */ multipartHandler(req, res) { const ctx = req.$ctx; ctx.meta.$multipart = {}; const promises = []; let numOfFiles = 0; let hasField = false; const busboyOptions = _.defaultsDeep({ headers: req.headers }, this.busboyConfig, this.route.opts.busboyConfig); const busboy = new Busboy(busboyOptions); busboy.on("file", (fieldname, file, filename, encoding, mimetype) => { file.on("limit", () => { // This file reached the file size limit. if (_.isFunction(busboyOptions.onFileSizeLimit)) { busboyOptions.onFileSizeLimit.call(this.service, file, busboy); } file.destroy(new PayloadTooLarge({ fieldname, filename, encoding, mimetype })); }); numOfFiles++; promises.push(ctx.call(this.action, file, _.defaultsDeep({}, this.route.opts.callOptions, { meta: { fieldname: fieldname, filename: filename, encoding: encoding, mimetype: mimetype, $params: req.$params, } })).catch(err => { file.resume(); // Drain file stream to continue processing form busboy.emit("error", err); return err; })); }); busboy.on("field", (field, value) => { hasField = true; ctx.meta.$multipart[field] = value; }); busboy.on("finish", async () => { /* istanbul ignore next */ if (!busboyOptions.empty && numOfFiles == 0) return this.service.sendError(req, res, new MoleculerClientError("File missing in the request")); // Call the action if no files but multipart fields if (numOfFiles == 0 && hasField) { promises.push(ctx.call(this.action, {}, _.defaultsDeep({}, this.route.opts.callOptions, { meta: { $params: req.$params, } }))); } try { let data = await this.service.Promise.all(promises); const fileLimit = busboyOptions.limits && busboyOptions.limits.files != null ? busboyOptions.limits.files : null; if (numOfFiles == 1 && fileLimit == 1) { // Remove the array wrapping data = data[0]; } if (this.route.onAfterCall) data = await this.route.onAfterCall.call(this, ctx, this.route, req, res, data); this.service.sendResponse(req, res, data, {}); } catch(err) { /* istanbul ignore next */ this.service.sendError(req, res, err); } }); /* istanbul ignore next */ busboy.on("error", err => { req.unpipe(req.busboy); req.resume(); this.service.sendError(req, res, err); }); // Add limit event handlers if (_.isFunction(busboyOptions.onPartsLimit)) { busboy.on("partsLimit", () => busboyOptions.onPartsLimit.call(this.service, busboy, this, this.service)); } if (_.isFunction(busboyOptions.onFilesLimit)) { busboy.on("filesLimit", () => busboyOptions.onFilesLimit.call(this.service, busboy, this, this.service)); } if (_.isFunction(busboyOptions.onFieldsLimit)) { busboy.on("fieldsLimit", () => busboyOptions.onFieldsLimit.call(this.service, busboy, this, this.service)); } req.pipe(busboy); } } module.exports = Alias;