express-intercept
Version:
Build Express middleware to intercept / replace / inspect / transform response
199 lines (198 loc) • 7.43 kB
JavaScript
;
// express-intercept.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.responseHandler = exports.requestHandler = void 0;
const stream_1 = require("stream");
const _handler_js_1 = require("./_handler.js");
const _compression_js_1 = require("./_compression.js");
const async_request_handler_1 = require("async-request-handler");
const requestHandler = errorHandler => {
return new RequestHandlerBuilder(errorHandler || defaultErrorHandler);
};
exports.requestHandler = requestHandler;
const responseHandler = errorHandler => {
return new ResponseHandlerBuilder(errorHandler || defaultErrorHandler);
};
exports.responseHandler = responseHandler;
const defaultErrorHandler = (err, req, res, next) => {
console.error(err);
// use .send("") instead of .end(), since Node.js v13
res.status(500).send("");
};
class RequestHandlerBuilder {
constructor(errorHandler) {
this._error = errorHandler;
}
_error;
/**
* It appends a test condition to perform the RequestHandler.
* Call this for multiple times to add multiple tests in AND condition.
* Those tests could avoid unnecessary work later.
*/
for(condition) {
this._for = AND(this._for, condition);
return this;
}
_for;
/**
* It returns a RequestHandler which connects multiple RequestHandlers.
* Use this after `requestHandler()` method but not after `responseHandler()`.
*/
use(handler, ...more) {
let { _for, _error } = this;
if (more.length) {
handler = (0, async_request_handler_1.ASYNC)(handler, async_request_handler_1.ASYNC.apply(null, more));
}
else {
handler = (0, async_request_handler_1.ASYNC)(handler);
}
if (_for)
handler = (0, async_request_handler_1.IF)(_for, handler);
if (_error)
handler = (0, async_request_handler_1.ASYNC)(handler, (0, async_request_handler_1.CATCH)(_error));
return handler;
}
/**
* It returns a RequestHandler to inspect express Request object (aka `req`).
* With `requestHandler()`, it works at request phase as normal RequestHandler works.
*/
getRequest(receiver) {
return this.use(async (req, res, next) => {
await receiver(req);
next();
});
}
}
class ResponseHandlerBuilder extends RequestHandlerBuilder {
use;
/**
* It appends a test condition to perform the RequestHandler.
* Call this for multiple times to add multiple tests in AND condition.
* Those tests could avoid unnecessary response interception work including additional buffering.
*/
if(condition) {
this._if = AND(this._if, condition);
return this;
}
_if;
/**
* It returns a RequestHandler to replace the response content body as a string.
* It gives a single string even when the response stream is chunked and/or compressed.
*/
replaceString(replacer) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
const body = payload.getString();
const replaced = await replacer(body, req, res);
if (body === replaced)
return; // nothing changed
payload.setString(replaced);
}));
}
/**
* It returns a RequestHandler to replace the response content body as a Buffer.
* It gives a single Buffer even when the response stream is chunked and/or compressed.
*/
replaceBuffer(replacer) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
let body = payload.getBuffer();
body = await replacer(body, req, res);
payload.setBuffer(body);
}));
}
/**
* It returns a RequestHandler to replace the response content body as a stream.Readable.
* Interceptor may need to decompress the response stream when compressed.
* Interceptor should return yet another stream.Readable to perform transform the stream.
* Interceptor would use stream.Transform for most cases as it is a Readable.
* Interceptor could return null or the upstream itself as given if transformation not happened.
*/
interceptStream(interceptor) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
return interceptor(payload, req, res);
}, () => new ReadablePayload()));
}
/**
* It returns a RequestHandler to retrieve the response content body as a string.
* It gives a single string even when the response stream is chunked and/or compressed.
*/
getString(receiver) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
const body = payload.getString();
await receiver(body, req, res);
}));
}
/**
* It returns a RequestHandler to retrieve the response content body as a Buffer.
* It gives a single Buffer even when the response stream is chunked and/or compressed.
*/
getBuffer(receiver) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
const body = payload.getBuffer();
await receiver(body, req, res);
}));
}
/**
* It returns a RequestHandler to inspect express Request object (aka `req`).
* With `responseHandler()`, it works at response returning phase after `res.send()` fired.
*/
getRequest(receiver) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
await receiver(req);
}));
}
/**
* It returns a RequestHandler to inspect express Response object (aka `res`) on its response returning phase after res.send() fired.
*/
getResponse(receiver) {
return super.use((0, _handler_js_1.buildResponseHandler)(this, async (payload, req, res) => {
await receiver(res);
}));
}
/**
* It returns a RequestHandler to compress the response content.
*/
compressResponse() {
return this.replaceBuffer((buf, req, res) => {
const encoding = (0, _compression_js_1.findEncoding)(req.header("Accept-Encoding"));
if (encoding) {
res.setHeader("Content-Encoding", encoding); // signal to compress with the encoding
}
return buf;
});
}
/**
* It returns a RequestHandler to decompress the response content.
*/
decompressResponse() {
return this.replaceBuffer((buf, req, res) => {
res.removeHeader("Content-Encoding"); // signal NOT to compress
return buf;
});
}
}
class ReadablePayload extends stream_1.Readable {
_read() {
// don't care
}
}
/**
* @private
*/
function AND(A, B) {
if (!A)
return B;
if (!B)
return A;
return (arg) => {
let result = A(arg);
// result: false
if (!result)
return false;
// result: true
if (!isThenable(result))
return B(arg);
// result: Promise<boolean>
return result.then(result => (result && B(arg)));
};
}
const isThenable = (value) => ("function" === typeof (value.then));