just-another-http-api 1.0.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/api.js CHANGED
@@ -1,134 +1,246 @@
1
- const restify = require ( 'restify' );
2
- const restifyErrors = require ( 'restify-errors' );
3
- const corsPlugin = require ( 'restify-cors-middleware2' );
1
+ const fastify = require ( 'fastify' );
4
2
  const recursiveRead = require ( 'recursive-readdir' );
5
3
  const packageJson = require ( './package.json' );
6
4
  const path = require ( 'path' );
7
- const multer = require ( 'multer' );
8
- const storage = multer.memoryStorage ();
9
5
 
10
- /**
11
- * See README for config setup.
12
- *
13
- * @param {Object} config The config, see readme for example configurations
14
- * @param {Function} every An optional agrument that accepts a function ready to receive an object. The function will be called everytime a endpoint is requested (good for analytical usage)
15
- * @param {RestifyServer} _server Optionally override the restify instance in this API and use your own. Accepts a `restify.createServer()` instance.
16
- */
17
- module.exports = async ( config, every = null, _server = null ) => {
6
+ const caching = require ( './src/cache' );
7
+ const uploads = require ( './src/upload' );
8
+ const auth = require ( './src/auth' );
9
+ const cors = require ( './src/cors' );
18
10
 
19
- if ( !config ) console.log ( 'JustAnother: WARNING: You\'ve initialised Just Another Http API without any config. This is not recommended.' );
11
+ let app;
20
12
 
21
- let upload;
22
- let server = _server;
13
+ module.exports = async ( config, _app = null ) => {
14
+ app = _app || await createServer ( config );
23
15
 
24
- if ( _server ) console.debug ( 'JustAnother: Using restify override instance provided.' );
25
- else {
26
- server = restify.createServer ( {
27
- name: packageJson.name,
28
- version: packageJson.version
29
- } );
16
+ const endpoints = await loadEndpoints ( config );
17
+ endpoints.forEach ( endpoint => registerEndpoint ( app, endpoint, config ) );
18
+
19
+ await app.listen ( { port: process.env.PORT || config?.port || 4001 } );
30
20
 
31
- if ( config?.bodyParser ) server.use ( restify.plugins.queryParser () );
32
- if ( config?.bodyParser ) server.use ( restify.plugins.bodyParser () );
33
- if ( config?.uploads && config?.uploads.enabled ) upload = multer ( { storage: storage } );
21
+ return app;
22
+ };
23
+
24
+ async function createServer ( config ) {
25
+ const app = fastify ( {
26
+ logger: config.logs || false,
27
+ name: config.name || packageJson.name,
28
+ } );
34
29
 
35
- if ( config?.cors ){
36
- const cors = corsPlugin ( config.cors );
37
- server.pre ( cors.preflight );
38
- server.use ( cors.actual );
30
+ try {
31
+
32
+ if ( config.cors ) {
33
+ app.register ( require ( '@fastify/cors' ), config.cors );
34
+ }
35
+ await uploads.initialiseUploads ( app, config );
36
+ await caching.initialiseCaching ( app, config );
37
+ await auth.initialiseAuth ( app, config );
38
+
39
+ if ( config.middleware && Array.isArray ( config.middleware ) ) {
40
+ for ( const func of config.middleware ) {
41
+ if ( typeof func === 'function' ) {
42
+ await app.register ( func );
43
+ }
44
+ }
39
45
  }
40
46
  }
47
+ catch ( error ) {
48
+ console.error ( 'Error during server initialization:', error );
49
+ throw error; // Rethrow the error to handle it at a higher level, if necessary
50
+ }
41
51
 
42
- server.on ( 'MethodNotAllowed', unknownMethodHandler );
52
+ return app;
53
+ }
43
54
 
55
+ async function loadEndpoints ( config ) {
44
56
  const files = await recursiveReadDir ( config?.docRoot || 'routes' );
45
- const endpoints = files.map ( ( filePath ) => ( {
57
+
58
+ return files.map ( filePath => ( {
46
59
  handlers: require ( path.resolve ( filePath ) ),
47
60
  path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
48
61
  } ) );
62
+ }
49
63
 
50
- endpoints.forEach ( endpoint => {
51
- Object.keys ( endpoint.handlers ).forEach ( method => {
52
- const endpointArgs = [
53
- endpoint.path,
54
- method === 'post' && upload ? upload.single ( 'file' ) : null
55
- ].filter ( Boolean );
56
-
57
- server[ method ] ( ...endpointArgs, async ( req, res ) => {
58
- try {
59
- const response = await endpoint.handlers[ method ] ( req );
60
-
61
- if ( every ) {
62
- every ( { path: endpoint.path, method, req } );
63
- }
64
-
65
- // If optional headers have been provided in the response add them here.
66
- if ( response.hasOwnProperty ( 'headers' ) ){
67
- res.set ( response.headers );
68
- }
69
-
70
- if ( response.hasOwnProperty ( 'redirect' ) ){
71
- res.redirect ( response.redirect.code, response.redirect.url, () => null );
72
- }
73
-
74
- if ( response.hasOwnProperty ( 'cache' ) ){
75
- res.cache ( response.cache.type, response.cache.options );
76
- }
77
-
78
- // If response.html is set, we want to send the HTML back as a raw string and set the content type.
79
- if ( response.hasOwnProperty ( 'html' ) ){
80
- res.sendRaw ( 200, response.html, { 'Content-Type': 'text/html' } );
81
- } //
82
- else if ( response.hasOwnProperty ( 'json' ) || response.hasOwnProperty ( 'body' ) || response.hasOwnProperty ( 'response' ) || typeof response === 'string' ){
83
- data = response?.json || response?.body || response?.response || response;
84
- res.setHeader ( 'Content-Type', 'application/json' );
85
- res.send ( method === 'post' ? 201 : 200, data );
86
- }
87
- else if ( response.hasOwnProperty ( 'error' ) ){
88
- console.error ( response.error );
89
- res.setHeader ( 'Content-Type', 'application/json' );
90
- res.send ( new restifyErrors.makeErrFromCode ( response?.error?.statusCode, response?.error?.message ) );
91
- }
92
- else if ( response.hasOwnProperty ( 'file' ) ){
93
- res.sendRaw ( response.file );
94
- }
95
- else if ( typeof response === 'object' ){
96
- res.send ( response ); //Try and send whatever it is
97
- }
98
- else if ( !response ){
99
- res.send ( 204 );
100
- }
101
- else {
102
- res.setHeader ( 'Content-Type', 'application/json' );
103
- res.send ( new restifyErrors.makeErrFromCode ( 500, `Just Another Http API did not understand the response provided for request: ${ method } to ${ endpoint.path }. Check your return value.` ) );
104
- }
105
-
106
- return;
107
- }
108
- catch ( error ){
109
- res.setHeader ( 'Content-Type', 'application/json' );
110
- if ( error instanceof Error ) {
111
- res.send ( new restifyErrors.InternalServerError ( { code: 500 }, error.stack.replace ( /\n/g, ' ' ) ) );
112
- }
113
- else {
114
- if ( error.code ) {
115
- res.send ( new restifyErrors.makeErrFromCode ( error.code, error.message ) );
116
- }
117
-
118
- res.send ( new restifyErrors.InternalServerError ( { code: 500 }, JSON.stringify ( error, null, 2 ) ) );
119
- }
64
+ function registerEndpoint ( app, endpoint, globalConfig ) {
65
+ Object.keys ( endpoint.handlers ).filter ( method => method !== 'config' ).forEach ( method => {
66
+ const handlerConfig = endpoint.handlers.config?.[ method ] || {};
67
+ const requiresAuth = handlerConfig?.requiresAuth !== undefined ? handlerConfig.requiresAuth : !!globalConfig?.auth?.requiresAuth;
120
68
 
121
- return;
122
- }
69
+ const preHandlers = [];
70
+ if ( requiresAuth ) {
71
+ preHandlers.push ( async ( req, reply ) => {
72
+ req.authConfig = handlerConfig.auth || globalConfig.auth;
73
+ await auth.checkAuth ( req, reply );
123
74
  } );
124
- } );
75
+ }
76
+
77
+ if ( globalConfig?.uploads?.enabled && handlerConfig?.upload?.enabled ) {
78
+ preHandlers.push ( uploads.handleUpload ( handlerConfig, globalConfig ) );
79
+ }
80
+ if ( handlerConfig?.cors !== undefined ) {
81
+ preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
82
+ }
83
+
84
+ const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
85
+ const handler = endpoint.handlers[ method ];
86
+ const wrappedHandler = fastifyHandlerWrapper ( handler, endpoint.handlers.config, globalConfig );
87
+
88
+ app[ fastifyMethod ] (
89
+ endpoint.path,
90
+ {
91
+ preHandler: preHandlers
92
+ },
93
+ wrappedHandler
94
+ );
125
95
  } );
126
-
127
- await server.listen ( process.env.PORT || config?.port || 4001 );
128
-
129
- return server;
96
+ }
97
+
98
+ function translateLegacyMethods ( method ) {
99
+ switch ( method ) {
100
+ case 'del':
101
+ return 'delete';
102
+ default:
103
+ return method;
104
+ }
105
+ }
106
+
107
+ function fastifyHandlerWrapper ( handler, config, globalConfig ) {
108
+ return async ( req, reply ) => {
109
+ try {
110
+ let response = await caching.checkRequestCache ( app, req, reply, config, globalConfig );
111
+
112
+ if ( !response ){
113
+ response = await handler ( req );
114
+ await caching.setRequestCache ( app, req, response, config, globalConfig );
115
+ }
116
+
117
+ handleResponse ( reply, response, req.method, req.routeOptions.url );
118
+ }
119
+ catch ( error ) {
120
+ handleError ( reply, error );
121
+ }
122
+ };
130
123
  };
131
124
 
125
+ function handleResponse ( reply, response, method, path ) {
126
+
127
+ if ( !response ) {
128
+ reply.code ( 204 ).send ();
129
+
130
+ return;
131
+ }
132
+
133
+ if ( typeof response !== 'object' || response === null ) {
134
+ handleNonObjectResponse ( reply, response, method );
135
+
136
+ return;
137
+ }
138
+
139
+ setResponseHeaders ( reply, response );
140
+ handleSpecialResponseTypes ( reply, response, method, path );
141
+ }
142
+
143
+ function setResponseHeaders ( reply, response ) {
144
+ if ( response.headers ) {
145
+ Object.entries ( response.headers ).forEach ( ( [ key, value ] ) => {
146
+ reply.header ( key, value );
147
+ } );
148
+ }
149
+ }
150
+
151
+ function handleSpecialResponseTypes ( reply, response, method, path ) {
152
+ if ( response.redirect ) {
153
+ reply.redirect ( 301, response.redirect.url );
154
+
155
+ return;
156
+ }
157
+
158
+ if ( response.html ) {
159
+ reply.code ( 200 ).type ( 'text/html' ).send ( response.html );
160
+
161
+ return;
162
+ }
163
+
164
+ if ( response.text ) {
165
+ reply.code ( 200 ).type ( 'text/plain' ).send ( response.text );
166
+
167
+ return;
168
+ }
169
+
170
+ if ( response.error ) {
171
+ handleErrorResponse ( reply, response.error );
172
+
173
+ return;
174
+ }
175
+
176
+ if ( response.file ) {
177
+ reply.send ( response.file );
178
+
179
+ return;
180
+ }
181
+
182
+ sendGenericResponse ( reply, response, method, path );
183
+ }
184
+
185
+ function sendGenericResponse ( reply, response, method, path ) {
186
+
187
+ const data = response.json ?? response.body ?? response.response ?? response;
188
+
189
+ if ( data !== undefined ) {
190
+ reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
191
+ }
192
+ else {
193
+ handleUnknownResponseType ( reply, method, path );
194
+ }
195
+
196
+ }
197
+
198
+ function handleErrorResponse ( reply, error ) {
199
+ console.error ( error );
200
+ const statusCode = error?.statusCode ?? 500;
201
+ reply.code ( statusCode ).type ( 'application/json' ).send ( { error: error.message } );
202
+ }
203
+
204
+ function handleUnknownResponseType ( reply, method, path ) {
205
+ reply.type ( 'application/json' ).code ( 500 ).send ( {
206
+ error: `Unrecognized response type for ${method} ${path}`
207
+ } );
208
+ }
209
+
210
+ function handleNonObjectResponse ( reply, response, method ) {
211
+ reply.type ( 'text/plain' ).code ( method === 'post' ? 201 : 200 ).send ( response.toString () );
212
+ }
213
+
214
+ function handleError ( reply, error ) {
215
+ // Set content type for the error response
216
+ reply.header ( 'Content-Type', 'application/json' );
217
+
218
+ if ( error instanceof Error ) {
219
+ // Send internal server error with the error stack
220
+ reply.status ( 500 ).send ( {
221
+ error: 'Internal Server Error',
222
+ message: error.message,
223
+ stack: error.stack
224
+ } );
225
+ }
226
+ else {
227
+ // Check if error object contains a specific status code
228
+ if ( error.code && typeof error.code === 'number' ) {
229
+ reply.status ( error.code ).send ( {
230
+ error: 'Error',
231
+ message: error.message
232
+ } );
233
+ }
234
+ else {
235
+ // Send a generic internal server error response
236
+ reply.status ( 500 ).send ( {
237
+ error: 'Internal Server Error',
238
+ message: 'An unknown error occurred'
239
+ } );
240
+ }
241
+ }
242
+ }
243
+
132
244
  const handlerPathToApiPath = ( path, docRoot ) => {
133
245
 
134
246
  const targetPath = path.replace ( docRoot.replace ( /.\//igm, '' ), '' ).split ( '/' );
@@ -152,28 +264,10 @@ const recursiveReadDir = async ( docRoot ) => {
152
264
  try {
153
265
  const files = await recursiveRead ( docRoot );
154
266
 
155
- // Remove all falsy values and reverse the array.
156
267
  return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
157
268
  }
158
269
  catch ( e ){
159
270
  console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
160
271
  throw e;
161
272
  }
162
- };
163
-
164
- const unknownMethodHandler = ( req, res ) => {
165
- if ( req.method.toLowerCase () === 'options' ) {
166
- const allowHeaders = [ '*' ];
167
-
168
- if ( res.methods.indexOf ( 'OPTIONS' ) === -1 ) res.methods.push ( 'OPTIONS' );
169
-
170
- res.header ( 'Access-Control-Allow-Credentials', true );
171
- res.header ( 'Access-Control-Allow-Headers', allowHeaders.join ( ', ' ) );
172
- res.header ( 'Access-Control-Allow-Methods', res.methods.join ( ', ' ) );
173
- res.header ( 'Access-Control-Allow-Origin', req.headers.origin );
174
-
175
- return res.send ( 204 );
176
- }
177
-
178
- return res.send ( new restifyErrors.MethodNotAllowedError ( { code: 405 }, `${ req.method } method is not available on this endpoint` ) );
179
273
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "just-another-http-api",
3
- "version": "1.0.5",
4
- "description": "A framework built on top of restify aimed at removing the need for any network or server configuration. ",
3
+ "version": "1.2.1",
4
+ "description": "A framework built on top of fastify aimed at removing the need for any network or server configuration. ",
5
5
  "homepage": "https://github.com/OllieEdge/just-another-http-api#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  "restapi",
17
17
  "restful",
18
18
  "rest",
19
- "restify",
19
+ "fastify",
20
20
  "http",
21
21
  "server",
22
22
  "just another",
@@ -25,15 +25,21 @@
25
25
  "author": "Oliver Edgington <oliver@edgington.com> (https://github.com/OllieEdge)",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "multer": "^1.4.2",
29
- "recursive-readdir": "^2.2.3",
30
- "restify": "^10.0.0",
31
- "restify-cors-middleware2": "^2.2.0",
32
- "restify-errors": "^8.0.0"
28
+ "@aws-sdk/lib-storage": "^3.454.0",
29
+ "@fastify/caching": "^8.3.0",
30
+ "@fastify/cors": "^8.4.1",
31
+ "@fastify/jwt": "^7.2.3",
32
+ "@fastify/redis": "^6.1.1",
33
+ "abstract-cache": "^1.0.1",
34
+ "abstract-cache-redis": "^2.0.0",
35
+ "fastify": "^4.24.3",
36
+ "fastify-multer": "^2.0.3",
37
+ "recursive-readdir": "^2.2.3"
33
38
  },
34
39
  "devDependencies": {
35
- "chai": "^4.3.7",
40
+ "chai": "^4.3.10",
36
41
  "chai-as-promised": "^7.1.1",
42
+ "eip-cloud-services": "^1.1.0",
37
43
  "mocha": "^10.2.0"
38
44
  }
39
45
  }
package/src/auth.js ADDED
@@ -0,0 +1,140 @@
1
+ let config;
2
+
3
+ exports.initialiseAuth = async ( app, _config ) => {
4
+ config = _config;
5
+
6
+ const authType = config?.auth?.type;
7
+
8
+ switch ( authType ) {
9
+ case 'jwt':
10
+
11
+ checkJwtIsAvailable ( config );
12
+
13
+ app.register ( require ( '@fastify/jwt' ), {
14
+ secret: config.auth.jwtSecret,
15
+ } );
16
+
17
+ app.decorate ( 'authenticate', async function ( request, reply ) {
18
+ try {
19
+ await request.jwtVerify ();
20
+ }
21
+ catch ( err ) {
22
+ if ( err.name === 'JsonWebTokenError' ) {
23
+ reply.status ( 400 ).send ( { error: 'Invalid Token' } );
24
+ }
25
+ else if ( err.name === 'TokenExpiredError' ) {
26
+ reply.status ( 401 ).send ( { error: 'Token Expired' } );
27
+ }
28
+ else {
29
+ reply.send ( err );
30
+ }
31
+ }
32
+ } );
33
+
34
+ app.post ( config?.auth?.tokenEndpoint || '/auth/login', async ( req, reply ) => {
35
+
36
+ const jwtToTokenise = await config.auth.jwtLoginHandle ( req.body );
37
+
38
+ if ( !jwtToTokenise ) {
39
+ reply.status ( 401 );
40
+ reply.send ( { error: 'Invalid credentials' } );
41
+
42
+ return;
43
+ }
44
+
45
+ const accessToken = app.jwt.sign ( { username: jwtToTokenise }, { expiresIn: config.auth.jwtExpiresIn } );
46
+ const refreshToken = config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ? app.jwt.sign ( { username: jwtToTokenise }, { expiresIn: config.auth.jwtRefreshExpiresIn } ) : null;
47
+ const expires = config.auth.jwtExpiresIn;
48
+
49
+ await config.auth.jwtStoreRefreshToken ( jwtToTokenise, refreshToken );
50
+
51
+ reply.send ( { user: jwtToTokenise, accessToken, refreshToken, expires } );
52
+
53
+ return;
54
+
55
+ } );
56
+
57
+ if ( config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ){
58
+ app.post ( config?.auth?.refreshTokenEndpoint || '/auth/refresh', async ( req, reply ) => {
59
+
60
+ const { user, refreshToken } = req.body;
61
+
62
+ const isValid = await config.auth.jwtRetrieveRefreshToken ( user, refreshToken );
63
+ if ( !isValid ) {
64
+ reply.status ( 401 ).send ( { error: 'Invalid refresh token' } );
65
+
66
+ return;
67
+ }
68
+
69
+ const accessToken = app.jwt.sign ( { username: user }, { expiresIn: config.auth.jwtExpiresIn } );
70
+ const newRefreshToken = config.auth.jwtEnabledRefreshTokens && checkJwtRefreshIsAvailable ( config ) ? app.jwt.sign ( { username: user }, { expiresIn: config.auth.jwtRefreshExpiresIn } ) : null;
71
+ const expires = config.auth.jwtExpiresIn;
72
+
73
+ await config.auth.jwtStoreRefreshToken ( user, newRefreshToken );
74
+
75
+ reply.send ( { user, accessToken, refreshToken: newRefreshToken, expires } );
76
+
77
+ return;
78
+
79
+ } );
80
+ }
81
+
82
+ break;
83
+ default:
84
+ return;
85
+ }
86
+
87
+ };
88
+
89
+ exports.checkAuth = async ( req, reply ) => {
90
+ const authType = config.auth.type; // authConfig is set in each route
91
+
92
+ try {
93
+ switch ( authType ) {
94
+ case 'jwt':
95
+ await req.jwtVerify ();
96
+ break;
97
+ case 'basic':
98
+ // Basic auth logic
99
+ break;
100
+ case 'oauth2':
101
+ // OAuth2 auth logic
102
+ break;
103
+ case 'token':
104
+ // Token-based auth logic
105
+ break;
106
+ default:
107
+ throw new Error ( 'Unsupported authentication type' );
108
+ }
109
+
110
+ return;
111
+ }
112
+ catch ( err ) {
113
+ reply.send ( err );
114
+ }
115
+ };
116
+
117
+ const checkJwtIsAvailable = ( config ) => {
118
+ if ( !config?.auth?.jwtLoginHandle || typeof config.auth.jwtLoginHandle !== 'function' ){
119
+ throw new Error ( '`auth.type` is set to "jwt", but `auth.jwtLoginHandle` is not a function or is not defined.' );
120
+ }
121
+ if ( !config?.auth?.jwtExpiresIn || typeof config.auth.jwtExpiresIn !== 'number' ){
122
+ throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtExpiresIn` is not a number or is not defined.' );
123
+ }
124
+
125
+ return true;
126
+ };
127
+
128
+ const checkJwtRefreshIsAvailable = ( config ) => {
129
+ if ( !config?.auth?.jwtStoreRefreshToken || typeof config.auth.jwtStoreRefreshToken !== 'function' ){
130
+ throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtStoreRefreshToken` is not a function or is not defined.' );
131
+ }
132
+ if ( !config?.auth?.jwtRetrieveRefreshToken || typeof config.auth.jwtRetrieveRefreshToken !== 'function' ){
133
+ throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtRetrieveRefreshToken` is not a function or is not defined.' );
134
+ }
135
+ if ( !config?.auth?.jwtRefreshExpiresIn || typeof config.auth.jwtRefreshExpiresIn !== 'number' ){
136
+ throw new Error ( '`auth.jwtEnabledRefreshTokens` is set to true, but `auth.jwtRefreshExpiresIn` is not a number or is not defined.' );
137
+ }
138
+
139
+ return true;
140
+ };
package/src/cache.js ADDED
@@ -0,0 +1,69 @@
1
+ const getCacheKey = ( config, req ) => `${config?.cache?.redisPrefix || ''}:${req.method.toLowerCase ()}:${req.routeOptions.url}:${JSON.stringify ( req.query )}`;
2
+
3
+ exports.initialiseCaching = async ( app, config ) => {
4
+
5
+ if ( config.cache && config.cache.enabled && config.cache.redisClient ) {
6
+
7
+ await app.register ( require ( '@fastify/redis' ), { client: config.cache.redisClient } );
8
+ await app.register ( require ( '@fastify/caching' ), require ( 'abstract-cache' ) ( {
9
+ useAwait: true,
10
+ driver: {
11
+ name: 'abstract-cache-redis',
12
+ options: { client: config.cache.redisClient }
13
+ }
14
+ } ) );
15
+
16
+ }
17
+
18
+ return;
19
+ };
20
+
21
+ exports.checkRequestCache = async ( app, req, reply, handleConfig, globalConfig ) => {
22
+
23
+ if ( globalConfig?.cache?.enabled && handleConfig?.[ req.method.toLowerCase () ]?.cache ) {
24
+
25
+ const cacheKey = getCacheKey ( globalConfig, req );
26
+ const script = `
27
+ local value = redis.call('GET', KEYS[1])
28
+ local ttl = redis.call('TTL', KEYS[1])
29
+ return {value, ttl}
30
+ `;
31
+ const result = await app.redis.eval ( script, 1, cacheKey );
32
+
33
+ if ( result[ 0 ] ) {
34
+ const cachedResponse = JSON.parse ( result[ 0 ] );
35
+ const ttl = result[ 1 ];
36
+ const maxAge = handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60;
37
+ const ageInSeconds = maxAge - ttl; // Calculate the age based on TTL and maxAge
38
+
39
+ if ( globalConfig?.cache?.addCacheHeaders ){
40
+ cachedResponse.headers ??= {};
41
+ cachedResponse.headers[ 'X-Cache' ] = 'HIT';
42
+ cachedResponse.headers[ 'X-Cache-Age' ] = ageInSeconds;
43
+ cachedResponse.headers[ 'X-Cache-Expires' ] = maxAge;
44
+ }
45
+
46
+ return cachedResponse;
47
+ }
48
+
49
+ }
50
+
51
+ return null;
52
+ };
53
+
54
+ exports.setRequestCache = async ( app, req, response, handleConfig, globalConfig ) => {
55
+
56
+ if ( globalConfig?.cache?.enabled && handleConfig?.[ req.method.toLowerCase () ]?.cache ){
57
+ const cacheKey = getCacheKey ( globalConfig, req );
58
+
59
+ await app.redis.set ( cacheKey, JSON.stringify ( response ), 'EX', handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60 );
60
+
61
+ if ( globalConfig?.cache?.addCacheHeaders ){
62
+ response.headers ??= {};
63
+ response.headers[ 'X-Cache' ] = 'MISS';
64
+ response.headers[ 'X-Cache-Age' ] = 0;
65
+ response.headers[ 'X-Cache-Expires' ] = handleConfig?.[ req.method.toLowerCase () ]?.expires || globalConfig?.cache?.expires || 60;
66
+ }
67
+
68
+ }
69
+ };
package/src/cors.js ADDED
@@ -0,0 +1,9 @@
1
+ exports.addCustomCors = ( handlerConfig ) => ( req, reply, done ) => {
2
+
3
+ Object.entries ( handlerConfig.cors ).forEach ( ( [ key, value ] ) => {
4
+ reply.header ( key, value );
5
+ } );
6
+
7
+ done ();
8
+
9
+ };