@nestjs/platform-fastify
Version:
Nest - modern, fast, powerful node.js web framework (@platform-fastify)
386 lines (385 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FastifyAdapter = void 0;
const common_1 = require("@nestjs/common");
const load_package_util_1 = require("@nestjs/common/utils/load-package.util");
const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
const http_adapter_1 = require("@nestjs/core/adapters/http-adapter");
const fastify_1 = require("fastify");
const Reply = require("fastify/lib/reply");
const symbols_1 = require("fastify/lib/symbols");
const pathToRegexp = require("path-to-regexp");
// `querystring` is used internally in fastify for registering urlencoded body parser.
const querystring_1 = require("querystring");
const constants_1 = require("../constants");
/**
* @publicApi
*/
class FastifyAdapter extends http_adapter_1.AbstractHttpAdapter {
get isParserRegistered() {
return !!this._isParserRegistered;
}
constructor(instanceOrOptions) {
super();
this.versionConstraint = {
name: 'version',
validate(value) {
if (!(0, shared_utils_1.isString)(value) && !Array.isArray(value)) {
throw new Error('Version constraint should be a string or an array of strings.');
}
},
storage() {
const versions = new Map();
return {
get(version) {
if (Array.isArray(version)) {
return versions.get(version.find(v => versions.has(v))) || null;
}
return versions.get(version) || null;
},
set(versionOrVersions, store) {
const storeVersionConstraint = (version) => versions.set(version, store);
if (Array.isArray(versionOrVersions))
versionOrVersions.forEach(storeVersionConstraint);
else
storeVersionConstraint(versionOrVersions);
},
del(version) {
if (Array.isArray(version)) {
version.forEach(v => versions.delete(v));
}
else {
versions.delete(version);
}
},
empty() {
versions.clear();
},
};
},
deriveConstraint: (req) => {
// Media Type (Accept Header) Versioning Handler
if (this.versioningOptions.type === common_1.VersioningType.MEDIA_TYPE) {
const MEDIA_TYPE_HEADER = 'Accept';
const acceptHeaderValue = (req.headers?.[MEDIA_TYPE_HEADER] || req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()]);
const acceptHeaderVersionParameter = acceptHeaderValue
? acceptHeaderValue.split(';')[1]
: '';
return (0, shared_utils_1.isUndefined)(acceptHeaderVersionParameter)
? common_1.VERSION_NEUTRAL // No version was supplied
: acceptHeaderVersionParameter.split(this.versioningOptions.key)[1];
}
// Header Versioning Handler
else if (this.versioningOptions.type === common_1.VersioningType.HEADER) {
const customHeaderVersionParameter = req.headers?.[this.versioningOptions.header] ||
req.headers?.[this.versioningOptions.header.toLowerCase()];
return (0, shared_utils_1.isUndefined)(customHeaderVersionParameter)
? common_1.VERSION_NEUTRAL // No version was supplied
: customHeaderVersionParameter;
}
// Custom Versioning Handler
else if (this.versioningOptions.type === common_1.VersioningType.CUSTOM) {
return this.versioningOptions.extractor(req);
}
return undefined;
},
mustMatchWhenDerived: false,
};
const instance = instanceOrOptions && instanceOrOptions.server
? instanceOrOptions
: (0, fastify_1.fastify)({
constraints: {
version: this.versionConstraint,
},
...instanceOrOptions,
});
this.setInstance(instance);
}
async init() {
if (this.isMiddieRegistered) {
return;
}
await this.registerMiddie();
}
listen(listenOptions, ...args) {
const isFirstArgTypeofFunction = typeof args[0] === 'function';
const callback = isFirstArgTypeofFunction ? args[0] : args[1];
let options;
if (typeof listenOptions === 'object' &&
(listenOptions.host !== undefined ||
listenOptions.port !== undefined ||
listenOptions.path !== undefined)) {
// First parameter is an object with a path, port and/or host attributes
options = listenOptions;
}
else {
options = {
port: +listenOptions,
};
}
if (!isFirstArgTypeofFunction) {
options.host = args[0];
}
return this.instance.listen(options, callback);
}
get(...args) {
return this.injectRouteOptions('get', ...args);
}
post(...args) {
return this.injectRouteOptions('post', ...args);
}
head(...args) {
return this.injectRouteOptions('head', ...args);
}
delete(...args) {
return this.injectRouteOptions('delete', ...args);
}
put(...args) {
return this.injectRouteOptions('put', ...args);
}
patch(...args) {
return this.injectRouteOptions('patch', ...args);
}
options(...args) {
return this.injectRouteOptions('options', ...args);
}
applyVersionFilter(handler, version, versioningOptions) {
if (!this.versioningOptions) {
this.versioningOptions = versioningOptions;
}
const versionedRoute = handler;
versionedRoute.version = version;
return versionedRoute;
}
reply(response, body, statusCode) {
const fastifyReply = this.isNativeResponse(response)
? new Reply(response, {
[symbols_1.kRouteContext]: {
preSerialization: null,
preValidation: [],
preHandler: [],
onSend: [],
onError: [],
},
}, {})
: response;
if (statusCode) {
fastifyReply.status(statusCode);
}
if (body instanceof common_1.StreamableFile) {
const streamHeaders = body.getHeaders();
if (fastifyReply.getHeader('Content-Type') === undefined &&
streamHeaders.type !== undefined) {
fastifyReply.header('Content-Type', streamHeaders.type);
}
if (fastifyReply.getHeader('Content-Disposition') === undefined &&
streamHeaders.disposition !== undefined) {
fastifyReply.header('Content-Disposition', streamHeaders.disposition);
}
if (fastifyReply.getHeader('Content-Length') === undefined &&
streamHeaders.length !== undefined) {
fastifyReply.header('Content-Length', streamHeaders.length);
}
body = body.getStream();
}
if (fastifyReply.getHeader('Content-Type') !== undefined &&
fastifyReply.getHeader('Content-Type') !== 'application/json' &&
body?.statusCode >= common_1.HttpStatus.BAD_REQUEST) {
common_1.Logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses", FastifyAdapter.name);
fastifyReply.header('Content-Type', 'application/json');
}
return fastifyReply.send(body);
}
status(response, statusCode) {
if (this.isNativeResponse(response)) {
response.statusCode = statusCode;
return response;
}
return response.code(statusCode);
}
end(response, message) {
response.raw.end(message);
}
render(response, view, options) {
return response && response.view(view, options);
}
redirect(response, statusCode, url) {
const code = statusCode ?? common_1.HttpStatus.FOUND;
return response.status(code).redirect(url);
}
setErrorHandler(handler) {
return this.instance.setErrorHandler(handler);
}
setNotFoundHandler(handler) {
return this.instance.setNotFoundHandler(handler);
}
getHttpServer() {
return this.instance.server;
}
getInstance() {
return this.instance;
}
register(plugin, opts) {
return this.instance.register(plugin, opts);
}
inject(opts) {
return this.instance.inject(opts);
}
async close() {
try {
return await this.instance.close();
}
catch (err) {
// Check if server is still running
if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
throw err;
}
return;
}
}
initHttpServer() {
this.httpServer = this.instance.server;
}
useStaticAssets(options) {
return this.register((0, load_package_util_1.loadPackage)('@fastify/static', 'FastifyAdapter.useStaticAssets()', () => require('@fastify/static')), options);
}
setViewEngine(options) {
if ((0, shared_utils_1.isString)(options)) {
new common_1.Logger('FastifyAdapter').error("setViewEngine() doesn't support a string argument.");
process.exit(1);
}
return this.register((0, load_package_util_1.loadPackage)('@fastify/view', 'FastifyAdapter.setViewEngine()', () => require('@fastify/view')), options);
}
isHeadersSent(response) {
return response.sent;
}
setHeader(response, name, value) {
return response.header(name, value);
}
getRequestHostname(request) {
return request.hostname;
}
getRequestMethod(request) {
return request.raw ? request.raw.method : request.method;
}
getRequestUrl(request) {
return this.getRequestOriginalUrl(request.raw || request);
}
enableCors(options) {
this.register(Promise.resolve().then(() => require('@fastify/cors')), options);
}
registerParserMiddleware(prefix, rawBody) {
if (this._isParserRegistered) {
return;
}
this.registerUrlencodedContentParser(rawBody);
this.registerJsonContentParser(rawBody);
this._isParserRegistered = true;
}
useBodyParser(type, rawBody, options, parser) {
const parserOptions = {
...(options || {}),
parseAs: 'buffer',
};
this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
if (parser) {
parser(req, body, done);
return;
}
done(null, body);
});
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
this._isParserRegistered = true;
}
async createMiddlewareFactory(requestMethod) {
if (!this.isMiddieRegistered) {
await this.registerMiddie();
}
return (path, callback) => {
let normalizedPath = path.endsWith('/*')
? `${path.slice(0, -1)}(.*)`
: path;
// Fallback to "(.*)" to support plugins like GraphQL
normalizedPath = normalizedPath === '/(.*)' ? '(.*)' : normalizedPath;
const re = pathToRegexp(normalizedPath);
// The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
// ref https://github.com/fastify/middie/pull/55
this.instance.use(normalizedPath, (req, res, next) => {
const queryParamsIndex = req.originalUrl.indexOf('?');
const pathname = queryParamsIndex >= 0
? req.originalUrl.slice(0, queryParamsIndex)
: req.originalUrl;
if (!re.exec(pathname + '/') && normalizedPath) {
return next();
}
return callback(req, res, next);
});
};
}
getType() {
return 'fastify';
}
registerWithPrefix(factory, prefix = '/') {
return this.instance.register(factory, { prefix });
}
isNativeResponse(response) {
return !('status' in response);
}
registerJsonContentParser(rawBody) {
const contentType = 'application/json';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.useBodyParser(contentType, withRawBody, { bodyLimit }, (req, body, done) => {
const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
defaultJsonParser(req, body, done);
});
}
registerUrlencodedContentParser(rawBody) {
const contentType = 'application/x-www-form-urlencoded';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.useBodyParser(contentType, withRawBody, { bodyLimit }, (_req, body, done) => {
done(null, (0, querystring_1.parse)(body.toString()));
});
}
async registerMiddie() {
this.isMiddieRegistered = true;
await this.register(Promise.resolve().then(() => require('@fastify/middie')));
}
getRequestOriginalUrl(rawRequest) {
return rawRequest.originalUrl || rawRequest.url;
}
injectRouteOptions(routerMethodKey, ...args) {
const handlerRef = args[args.length - 1];
const isVersioned = !(0, shared_utils_1.isUndefined)(handlerRef.version) &&
handlerRef.version !== common_1.VERSION_NEUTRAL;
const routeConfig = Reflect.getMetadata(constants_1.FASTIFY_ROUTE_CONFIG_METADATA, handlerRef);
const hasConfig = !(0, shared_utils_1.isUndefined)(routeConfig);
if (isVersioned || hasConfig) {
const isPathAndRouteTuple = args.length === 2;
if (isPathAndRouteTuple) {
const options = {
...(isVersioned && {
constraints: {
version: handlerRef.version,
},
}),
...(hasConfig && {
config: {
...routeConfig,
},
}),
};
const path = args[0];
return this.instance[routerMethodKey](path, options, handlerRef);
}
}
return this.instance[routerMethodKey](...args);
}
}
exports.FastifyAdapter = FastifyAdapter;