just-another-http-api
Version:
A framework built on top of fastify aimed at removing the need for any network or server configuration.
298 lines (241 loc) • 9.9 kB
JavaScript
const fastify = require ( 'fastify' );
const recursiveRead = require ( 'recursive-readdir' );
const packageJson = require ( './package.json' );
const path = require ( 'path' );
const caching = require ( './src/cache' );
const uploads = require ( './src/upload' );
const auth = require ( './src/auth' );
const cors = require ( './src/cors' );
const preventDuplicateRequests = require('./utils/preventDuplicateRequest');
let app;
module.exports = async ( config, _app = null ) => {
app = _app || await createServer ( config );
const endpoints = await loadEndpoints ( config );
endpoints.forEach ( endpoint => registerEndpoint ( app, endpoint, config ) );
await app.listen ( { port: process.env.PORT || config?.port || 4001 } );
return app;
};
async function createServer ( config ) {
const app = fastify ( {
logger: config.logs || false,
name: config.name || packageJson.name,
...( config?.fastify || {} )
} );
try {
if ( config.cors ) {
app.register ( require ( '@fastify/cors' ), config.cors );
}
if ( config.websocket?.enabled ){
app.register ( require ( '@fastify/websocket' ), config.websocket?.options );
}
if (config.cookies?.enabled) {
app.register ( require ( '@fastify/cookie' ), config.cookies );
}
await uploads.initialiseUploads ( app, config );
await caching.initialiseCaching ( app, config );
await auth.initialiseAuth ( app, config );
if ( config.middleware && Array.isArray ( config.middleware ) ) {
for ( const func of config.middleware ) {
if ( typeof func === 'function' ) {
await app.register ( func );
}
}
}
}
catch ( error ) {
console.error ( 'Error during server initialization:', error );
throw error; // Rethrow the error to handle it at a higher level, if necessary
}
return app;
}
async function loadEndpoints ( config ) {
const files = await recursiveReadDir ( config?.docRoot || 'routes' );
return files.map ( filePath => ( {
handlers: require ( path.resolve ( filePath ) ),
path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
} ) );
}
function registerEndpoint ( app, endpoint, globalConfig ) {
Object.keys ( endpoint.handlers ).filter ( method => method !== 'config' ).forEach ( method => {
const handlerConfig = endpoint.handlers.config?.[ method ] || {};
const requiresAuth = handlerConfig?.requiresAuth !== undefined ? handlerConfig.requiresAuth : !!globalConfig?.auth?.requiresAuth;
const preHandlers = [];
if ( requiresAuth ) {
preHandlers.push ( async ( req, reply ) => {
req.authConfig = handlerConfig.auth || globalConfig.auth;
await auth.checkAuth ( req, reply );
} );
}
if ( globalConfig?.uploads?.enabled && handlerConfig?.upload?.enabled ) {
preHandlers.push ( uploads.handleUpload ( handlerConfig, globalConfig ) );
}
if ( handlerConfig?.cors !== undefined ) {
preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
}
if ( method.toLowerCase() === 'post' && handlerConfig?.preventDuplicate ) {
preHandlers.push( preventDuplicateRequests );
}
const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
const handler = endpoint.handlers[ method ];
const wrappedHandler = fastifyHandlerWrapper ( handler, endpoint.handlers.config, globalConfig, method );
app[ fastifyMethod ] (
endpoint.path,
{
preHandler: preHandlers,
websocket: handlerConfig?.websocket || false,
bodyLimit: handlerConfig?.bodyLimit || undefined,
},
wrappedHandler
);
} );
}
function translateLegacyMethods ( method ) {
switch ( method ) {
case 'del':
return 'delete';
default:
return method;
}
}
function fastifyHandlerWrapper ( handler, config, globalConfig, method ) {
if ( config?.[ method ]?.websocket ){
return handler;
}
else {
return async ( req, reply ) => {
try {
let response;
if ( globalConfig?.cache?.enabled ){
const request = { method: req.method, query: JSON.parse ( JSON.stringify ( req.query ) ), routeOptions: req.routeOptions, params: JSON.parse ( JSON.stringify ( req.params ) ) };
response = await caching.checkRequestCache ( app, request, reply, config, globalConfig );
if ( !response ){
response = await handler ( req, { invalidateRequest: ( _req ) => caching.invalidateRequestCache( app, _req, globalConfig ) } );
await caching.setRequestCache ( app, request, response, config, globalConfig );
}
}
else {
response = await handler ( req );
}
handleResponse ( reply, response, req.method, req.routeOptions.url );
}
catch ( error ) {
handleError ( reply, error );
}
};
}
};
function handleResponse ( reply, response, method, path ) {
if ( !response ) {
reply.code ( 204 ).send ();
return;
}
if ( typeof response !== 'object' || response === null ) {
handleNonObjectResponse ( reply, response, method );
return;
}
setResponseHeaders ( reply, response );
handleSpecialResponseTypes ( reply, response, method, path );
}
function setResponseHeaders ( reply, response ) {
if ( response.headers ) {
Object.entries ( response.headers ).forEach ( ( [ key, value ] ) => {
reply.header ( key, value );
} );
}
}
function handleSpecialResponseTypes ( reply, response, method, path ) {
if ( response.redirect ) {
reply.redirect ( 301, response.redirect.url );
return;
}
if ( response.html ) {
reply.code ( 200 ).type ( 'text/html' ).send ( response.html );
return;
}
if ( response.text ) {
reply.code ( 200 ).type ( 'text/plain' ).send ( response.text );
return;
}
if ( response.error ) {
handleErrorResponse ( reply, response.error );
return;
}
if ( response.file ) {
reply.send ( response.file );
return;
}
sendGenericResponse ( reply, response, method, path );
}
function sendGenericResponse ( reply, response, method, path ) {
const data = response.json !== undefined ? response.json : response.body ?? response.response ?? response;
if ( data !== undefined ) {
reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
}
else {
handleUnknownResponseType ( reply, method, path );
}
}
function handleErrorResponse ( reply, error ) {
console.error ( error );
const statusCode = error?.statusCode ?? 500;
reply.code ( statusCode ).type ( 'application/json' ).send ( { error: error.message } );
}
function handleUnknownResponseType ( reply, method, path ) {
reply.type ( 'application/json' ).code ( 500 ).send ( {
error: `Unrecognized response type for ${method} ${path}`
} );
}
function handleNonObjectResponse ( reply, response, method ) {
reply.type ( 'text/plain' ).code ( method === 'post' ? 201 : 200 ).send ( response.toString () );
}
function handleError ( reply, error ) {
// Set content type for the error response
reply.header ( 'Content-Type', 'application/json' );
if ( error instanceof Error ) {
// Send internal server error with the error stack
reply.status ( 500 ).send ( {
error: 'Internal Server Error',
message: error.message,
stack: error.stack
} );
}
else {
// Check if error object contains a specific status code
if ( error.code && typeof error.code === 'number' ) {
reply.status ( error.code ).send ( {
error: 'Error',
message: error.message
} );
}
else {
// Send a generic internal server error response
reply.status ( 500 ).send ( {
error: 'Internal Server Error',
message: 'An unknown error occurred'
} );
}
}
}
const handlerPathToApiPath = ( path, docRoot ) => {
const targetPath = path.replace ( docRoot.replace ( /.\//igm, '' ), '' ).split ( '/' );
return '/' + targetPath.map ( subPath => {
if ( subPath.includes ( 'index.js' ) && path.substr ( path.lastIndexOf ( '/' ) ).includes ( 'index' ) ) return null;
if ( ( subPath.includes ( '-' ) || subPath.includes ( 'Id' ) ) && ( subPath.includes ( '.js' ) || subPath.includes ( 'Id' ) ) ) {
subPath = subPath.split ( '-' ).map ( urlParameter => {
urlParameter = urlParameter.replace ( /-/igm, '' );
return ':' + urlParameter;
} ).join ( '/' );
}
return subPath.replace ( /.js/igm, '' );
} ).filter ( Boolean ).join ( '/' );
};
const recursiveReadDir = async ( docRoot ) => {
try {
const files = await recursiveRead ( docRoot );
return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
}
catch ( e ){
console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
throw e;
}
};