src/components/endpoint.js
import {isPlainObject, isFunction, isString} from '../utils/validators'
import {findBy, getBy, getId} from '../utils/data'
import {filter} from 'ramda';
function validateEndpoint(config) {
if (!config || !isPlainObject(config)) throw new Error('Invalid configuration object. See documentation for help.');
if (!config.path || typeof(config.path) !== 'string') throw new Error('Invalid path. Expected a string.');
}
/**
* This component is responsible for generating the correct routes for the endpoint and it's child endpoints.
* It also stores the current state of the endpoint.
*/
export class Endpoint {
_parent;
_identifierKey;
_identifierFunc;
_path;
_methods;
_nested = [];
_state;
_routes = [];
_nestedRoutes = [];
/**
* Creates a new instance of Endpoint.
* @param {EndpointConfig} config The configuration for the endpoint.
* @param {Endpoint} [parent] The parent endpoint of this endpoint.
*/
constructor(config, parent) {
validateEndpoint(config);
this._parent = parent;
this._identifierKey = config.identifierKey || 'id';
this._identifierFunc = config.identifierFunc && isFunction(config.identifierFunc) ? config.identifierFunc : getId;
this._path = config.path;
this._methods = config.methods;
this._state = config.state || [];
this.createRoutes();
if (config.nested && Array.isArray(config.nested)) {
this._nested = config.nested.map(endpointConfig =>
new Endpoint({...endpointConfig, path: `${this._path}/${endpointConfig.path}`}, this));
this._nestedRoutes = this._nested.reduce(((routes, endpoint) => routes.concat(endpoint.routes)), this._nestedRoutes);
}
}
/**
* Generates the routes for the endpoint based on {@link EndpointConfig.methods}.
*/
createRoutes() {
const customRoutes = filter(isFunction)(this._methods);
const standardRoutes = filter(isString)(this._methods);
customRoutes.forEach(routeFactory => this._routes.push(routeFactory('/' + this._path, this._state)));
standardRoutes.map(method => {
switch (method) {
case 'GET_ALL':
if (!Array.isArray(this._state)) throw new Error('Misconfigured route. GET_ALL state should be an array');
this._routes.push({
method: 'GET',
path: `/${this._path}`,
handler: (request, reply) => reply(this._state)
});
break;
case 'GET_ONE':
if (Array.isArray(this._state)) throw new Error('Misconfigured route. GET_ONE state should be an object');
this._routes.push({
method: 'GET',
path: `/${this._path}`,
handler: (request, reply) => reply(this._state)
});
break;
case 'GET_BY_KEY':
let handler;
if (Array.isArray(this._state)) {
handler = (request, reply) => {
const id = request.params[this._identifierKey];
const resource = getBy(this._identifierKey, id)(this._state);
if (resource === undefined) return reply('Resource not found.').code(404);
return reply(resource);
}
} else {
handler = (request, reply) => {
const id = request.params[this._identifierKey];
const resource = this._state[id];
if (resource === undefined) return reply('Resource not found.').code(404);
return reply(resource);
}
}
this._routes.push({
method: 'GET',
path: `/${this._path}/{id}`,
handler
});
break;
case 'POST':
if (!Array.isArray(this._state)) throw new Error('Misconfigured route. POST state should be an array');
this._routes.push({
method: 'POST',
path: `/${this._path}`,
handler: (request, reply) => {
const nextId = this._identifierFunc(this._state);
const newResource = {id: nextId, ...request.payload};
this._state.push(newResource);
return reply('')
.code(201)
.header('Location', `http://localhost:3000/${this._path}/${newResource.id}/`);
}
});
break;
case 'PUT':
if (!Array.isArray(this._state)) throw new Error('Misconfigured route. PUT state should be an array');
this._routes.push({
method: 'PUT',
path: `/${this._path}/{id}`,
handler: (request, reply) => {
const currentResourceIndex = findBy(this._identifierKey, request.params[this._identifierKey])(this._state);
if (currentResourceIndex === -1) throw new Error('Resource not found.').code(404);
const currentResource = this._state[currentResourceIndex];
this._state[currentResourceIndex] = {id: currentResource.id, ...request.payload};
reply('');
}
});
break;
case 'DELETE':
if (!Array.isArray(this._state)) throw new Error('Misconfigured route. DELETE state should be an array');
this._routes.push({
method: 'DELETE',
path: `/${this._path}/{id}`,
handler: (request, reply) => {
const currentResourceIndex = findBy(this._identifierKey, request.params[this._identifierKey])(this._state);
if (currentResourceIndex === -1) throw new Error('Resource not found.').code(404);
const deletedResource = this._state.splice(currentResourceIndex, 1)[0];
reply(deletedResource);
}
});
break;
default:
throw new Error('Unknown HTTP method: ' + method);
}
});
}
/**
* Gets the generated routes for the endpoint.
* @returns {Array}
*/
get route() {
return this._routes;
}
/**
* Gets the generates routes for the endpoint and all its child endpoints.
* @returns {Array.<T>}
*/
get routes() {
return this._nestedRoutes.concat(...this._routes);
}
/**
* Gets the current configuration and state of the endpoint.
* @returns {EndpointConfig}
*/
get currentState() {
return Object.assign({}, {
path: this._path,
methods: this._methods,
state: this._state,
nested: this._nested.map(nestedEndpoint => nestedEndpoint.currentState)
});
}
}