@athenna/http
Version:
The Athenna Http server. Built on top of fastify.
499 lines (498 loc) • 14.7 kB
JavaScript
/**
* @athenna/http
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { Is, Options, Macroable, Route as RouteHelper } from '@athenna/common';
import { UndefinedMethodException } from '#src/exceptions/UndefinedMethodException';
import { NotFoundValidatorException } from '#src/exceptions/NotFoundValidatorException';
import { NotFoundMiddlewareException } from '#src/exceptions/NotFoundMiddlewareException';
export class Route extends Macroable {
constructor(url, methods, handler) {
super();
this.route = {
url,
methods,
deleted: false,
prefixes: [],
middlewares: {
middlewares: [],
terminators: [],
interceptors: []
},
fastify: { schema: {}, config: {} }
};
if (Is.String(handler)) {
const [controller, method] = handler.split('.');
const dependency = ioc.safeUse(`App/Http/Controllers/${controller}`);
if (!dependency[method]) {
throw new UndefinedMethodException(method, controller);
}
this.route.handler = (...args) => {
const service = ioc.safeUse(`App/Http/Controllers/${controller}`);
if (!service[method]) {
throw new UndefinedMethodException(method, controller);
}
return service[method].bind(dependency)(...args);
};
}
else {
this.route.handler = handler;
}
RouteHelper.getParamsName(url).forEach(param => this.param(param));
}
/**
* Set the route name.
*
* @example
* ```ts
* Route.name('profile')
* ```
*/
name(name) {
this.route.name = name;
// eslint-disable-next-line
// @ts-ignore
this.route.fastify.config.name = name;
return this;
}
/**
* Set fastify vanilla route options.
*
* @example
* ```ts
* Route.vanillaOptions({ schema: {} })
* ```
*/
vanillaOptions(options) {
this.route.fastify = {
...this.route.fastify,
...options
};
return this;
}
/**
* Set a prefix for the route.
*
* @example
* ```ts
* Route.prefix('/api/v1')
* ```
*/
prefix(prefix) {
this.route.prefixes.push(prefix);
return this;
}
/**
* Set a named validator, validator closure or a MiddlewareContract implementation
* in the route.
*/
validator(validator, prepend = false) {
const insertionType = prepend ? 'unshift' : 'push';
if (Is.String(validator)) {
const namedAlias = `App/Validators/Names/${validator}`;
const alias = `App/Validators/${validator}`;
if (!ioc.has(namedAlias) && !ioc.has(alias)) {
throw new NotFoundValidatorException(alias, namedAlias);
}
this.route.middlewares.middlewares[insertionType]((...args) => {
const mid = ioc.use(namedAlias) || ioc.safeUse(alias);
return mid.handle.bind(mid)(...args);
});
return this;
}
if (Is.Function(validator)) {
this.route.middlewares.middlewares[insertionType](validator);
return this;
}
this.route.middlewares.middlewares[insertionType](validator.handle.bind(validator));
return this;
}
/**
* Set a named middleware, middleware closure or a MiddlewareContract implementation
* in the route.
*/
middleware(middleware, prepend = false) {
const insertionType = prepend ? 'unshift' : 'push';
if (Is.String(middleware)) {
const namedAlias = `App/Http/Middlewares/Names/${middleware}`;
const alias = `App/Http/Middlewares/${middleware}`;
if (!ioc.has(namedAlias) && !ioc.has(alias)) {
throw new NotFoundMiddlewareException(alias, namedAlias);
}
this.route.middlewares.middlewares[insertionType]((...args) => {
const mid = ioc.use(namedAlias) || ioc.safeUse(alias);
return mid.handle.bind(mid)(...args);
});
return this;
}
if (Is.Function(middleware)) {
this.route.middlewares.middlewares[insertionType](middleware);
return this;
}
this.route.middlewares.middlewares[insertionType](middleware.handle.bind(middleware));
return this;
}
/**
* Set a named interceptor, interceptor closure or a InterceptorContract implementation
* in the route.
*/
interceptor(interceptor, prepend = false) {
const insertionType = prepend ? 'unshift' : 'push';
if (Is.String(interceptor)) {
const namedAlias = `App/Http/Interceptors/Names/${interceptor}`;
const alias = `App/Http/Interceptors/${interceptor}`;
if (!ioc.has(namedAlias) && !ioc.has(alias)) {
throw new NotFoundMiddlewareException(alias, namedAlias);
}
this.route.middlewares.interceptors[insertionType]((...args) => {
const mid = ioc.use(namedAlias) || ioc.safeUse(alias);
return mid.intercept.bind(mid)(...args);
});
return this;
}
if (Is.Function(interceptor)) {
this.route.middlewares.interceptors[insertionType](interceptor);
return this;
}
this.route.middlewares.interceptors[insertionType](interceptor.intercept.bind(interceptor));
return this;
}
/**
* Set a named terminator, terminator closure or a TerminatorContract implementation
* in the route.
*/
terminator(terminator, prepend = false) {
const insertionType = prepend ? 'unshift' : 'push';
if (Is.String(terminator)) {
const namedAlias = `App/Http/Terminators/Names/${terminator}`;
const alias = `App/Http/Terminators/${terminator}`;
if (!ioc.has(namedAlias) && !ioc.has(alias)) {
throw new NotFoundMiddlewareException(alias, namedAlias);
}
this.route.middlewares.terminators[insertionType]((...args) => {
const mid = ioc.use(namedAlias) || ioc.safeUse(alias);
return mid.terminate.bind(mid)(...args);
});
return this;
}
if (Is.Function(terminator)) {
this.route.middlewares.terminators[insertionType](terminator);
return this;
}
this.route.middlewares.terminators[insertionType](terminator.terminate.bind(terminator));
return this;
}
/**
* Set up all helmet options for route.
*
* @example
* ```ts
* Route.helmet({
* dnsPrefetchControl: { allow: true }
* })
* ```
*/
helmet(options) {
this.route.fastify.helmet = options;
return this;
}
/**
* Set up all schema options for route.
*
* @example
* ```ts
* Route.schema({
* body: {
* type: 'array',
* items: {
* oneOf: [{ type: 'string', description: 'User name' }]
* }
* }
* })
* ```
*/
schema(options) {
this.route.fastify.schema = options;
return this;
}
/**
* Set up all rate limit options for route.
*
* @example
* ```ts
* Route.rateLimit({
* max: 3,
* timeWindow: '1 minute'
* })
* ```
*/
rateLimit(options) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.config.rateLimit = options;
return this;
}
/**
* Hide the route in swagger docs.
*
* @example
* ```ts
* Route.hide()
* ```
*/
hide() {
this.route.fastify.schema.hide = true;
return this;
}
/**
* Set the route as deprecated in swagger docs.
*
* @example
* ```ts
* Route.deprecated()
* ```
*/
deprecated() {
this.route.fastify.schema.deprecated = true;
return this;
}
/**
* Set a summary for the route swagger docs.
*
* @example
* ```ts
* Route.summary('List all users')
* ```
*/
summary(summary) {
this.route.fastify.schema.summary = summary;
return this;
}
/**
* Set a description for the route swagger docs.
*
* @example
* ```ts
* Route.description('List all users')
* ```
*/
description(description) {
this.route.fastify.schema.description = description;
return this;
}
/**
* Set tags for the route swagger docs.
*
* @example
* ```ts
* Route.tags(['v1'])
* ```
*/
tags(tags) {
if (!this.route.fastify.schema.tags) {
this.route.fastify.schema.tags = [];
}
tags.forEach(tag => this.route.fastify.schema.tags.push(tag));
return this;
}
/**
* Set body param for the route swagger docs.
*
* @example
* ```ts
* Route.body('name', {
* type: 'string',
* description: 'User name'
* })
* ```
*/
body(name, body = {}) {
if (!this.route.fastify.schema.body) {
this.route.fastify.schema.body = {
type: 'object',
properties: {}
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.schema.body.properties[name] = body;
return this;
}
/**
* Set param for the route swagger docs.
*
* @example
* ```ts
* Route.param('id', { description: 'Set the user id' })
* ```
*/
param(name, param = {}) {
if (!this.route.fastify.schema.params) {
this.route.fastify.schema.params = {
type: 'object',
properties: {}
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.schema.params.properties[name] = param;
return this;
}
/**
* Set query string for the route swagger docs.
*
* @example
* ```ts
* Route.queryString('page', { description: 'Set the pagination page' })
* ```
*/
queryString(name, queryString = {}) {
if (!this.route.fastify.schema.querystring) {
this.route.fastify.schema.querystring = {
type: 'object',
properties: {}
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.route.fastify.schema.querystring.properties[name] = queryString;
return this;
}
/**
* Set response for the route swagger docs.
*
* @example
* For objects
* ```ts
* Route.response(200, {
* description: 'Return one user',
* properties: {
* name: { type: 'string', description: 'User name' }
* }
* })
* ```
*
* For arrays
* ```ts
* Route.response(200, {
* description: 'Return users paginated',
* schema: {
* type: 'array'
* oneOf: [
* { type: 'string', description: 'User name' }
* ]
* }
* })
* ```
*/
response(statusCode, response) {
response = Options.create(response, {
description: `Status code ${statusCode} response`
});
if (!this.route.fastify.schema.response) {
this.route.fastify.schema.response = {};
}
this.route.fastify.schema.response[statusCode] = response;
return this;
}
/**
* Set the route security in swagger docs.
*
* @example
* ```ts
* Route.security('apiToken', [])
* ```
*/
security(key, values) {
if (!this.route.fastify.schema.security) {
this.route.fastify.schema.security = [];
}
this.route.fastify.schema.security.push({ [key]: values });
return this;
}
/**
* Set the external documentation url in swagger docs.
*
* @example
* ```ts
* Route.externalDocs('https://github.com/athennaio/docs', 'Athenna Framework Documentation')
* ```
*/
externalDocs(url, description) {
if (!this.route.fastify.schema.externalDocs) {
this.route.fastify.schema.externalDocs = {
url: '',
description: ''
};
}
this.route.fastify.schema.externalDocs.url = url;
this.route.fastify.schema.externalDocs.description = description;
return this;
}
/**
* Set the type of the content that the API consumes in swagger docs.
*
* @example
* ```ts
* Route.consumes(['json', 'yaml', ...])
* ```
*/
consumes(consumes) {
this.route.fastify.schema.consumes = consumes;
return this;
}
/**
* Set the type of the content that the API produces in swagger docs.
*
* @example
* ```ts
* Route.produces(['json', 'yaml', ...])
* ```
*/
produces(produces) {
this.route.fastify.schema.produces = produces;
return this;
}
/**
* Get the route as JSON.
*
* @example
* ```ts
* Route.toJSON()
* ```
*/
toJSON() {
return {
url: this.getUrl(),
methods: this.route.methods,
name: this.route.name,
deleted: this.route.deleted,
prefixes: this.route.prefixes,
handler: this.route.handler,
middlewares: this.route.middlewares,
fastify: this.route.fastify
};
}
getUrl() {
const url = this.removeSlashes(this.route.url);
const prefix = this.route.prefixes
.reverse()
.map(p => this.removeSlashes(p))
.join('');
return prefix ? `${prefix}${url === '/' ? '' : url}` : url;
}
removeSlashes(url) {
if (url === '/') {
return url;
}
const matcher = url => `/${url.replace(/^\//, '').replace(/\/$/, '')}`;
if (Is.Array(url)) {
return url.map(u => matcher(u));
}
return matcher(url);
}
}