just-another-http-api 1.0.5 → 1.2.0

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,133 +1,262 @@
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 } );
20
+
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
+ } );
30
29
 
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 } );
30
+ try {
31
+
32
+ if ( config.cors ) {
33
+ app.register ( require ( '@fastify/cors' ), ( instance ) => {
34
+ return ( req, callback ) => {
35
+ const corsOptions = {
36
+ // This is NOT recommended for production as it enables reflection exploits
37
+ origin: true
38
+ };
34
39
 
35
- if ( config?.cors ){
36
- const cors = corsPlugin ( config.cors );
37
- server.pre ( cors.preflight );
38
- server.use ( cors.actual );
40
+ console.log ( 'hbawfjawhv' );
41
+
42
+ // // do not include CORS headers for requests from localhost
43
+ // if ( /^localhost$/m.test ( req.headers.origin ) ) {
44
+ // corsOptions.origin = false;
45
+ // }
46
+
47
+ // callback expects two parameters: error and options
48
+ callback ( null, corsOptions );
49
+ };
50
+ } );
51
+ }
52
+ await uploads.initialiseUploads ( app, config );
53
+ await caching.initialiseCaching ( app, config );
54
+ await auth.initialiseAuth ( app, config );
55
+
56
+ if ( config.middleware && Array.isArray ( config.middleware ) ) {
57
+ for ( const func of config.middleware ) {
58
+ if ( typeof func === 'function' ) {
59
+ await app.register ( func );
60
+ }
61
+ }
39
62
  }
40
63
  }
64
+ catch ( error ) {
65
+ console.error ( 'Error during server initialization:', error );
66
+ throw error; // Rethrow the error to handle it at a higher level, if necessary
67
+ }
41
68
 
42
- server.on ( 'MethodNotAllowed', unknownMethodHandler );
69
+ return app;
70
+ }
43
71
 
72
+ async function loadEndpoints ( config ) {
44
73
  const files = await recursiveReadDir ( config?.docRoot || 'routes' );
45
- const endpoints = files.map ( ( filePath ) => ( {
74
+
75
+ return files.map ( filePath => ( {
46
76
  handlers: require ( path.resolve ( filePath ) ),
47
77
  path: handlerPathToApiPath ( filePath, config?.docRoot || 'routes' )
48
78
  } ) );
79
+ }
49
80
 
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 );
81
+ function registerEndpoint ( app, endpoint, globalConfig ) {
82
+ Object.keys ( endpoint.handlers ).filter ( method => method !== 'config' ).forEach ( method => {
83
+ const handlerConfig = endpoint.handlers.config?.[ method ] || {};
84
+ const requiresAuth = handlerConfig?.requiresAuth !== undefined ? handlerConfig.requiresAuth : !!globalConfig?.auth?.requiresAuth;
56
85
 
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
- }
86
+ const preHandlers = [];
87
+ if ( requiresAuth ) {
88
+ preHandlers.push ( async ( req, reply ) => {
89
+ req.authConfig = handlerConfig.auth || globalConfig.auth;
90
+ await auth.checkAuth ( req, reply );
91
+ } );
92
+ }
93
+
94
+ if ( globalConfig?.uploads?.enabled && handlerConfig?.upload?.enabled ) {
95
+ preHandlers.push ( uploads.handleUpload ( handlerConfig, globalConfig ) );
96
+ }
97
+ if ( handlerConfig?.cors !== undefined ) {
98
+ preHandlers.push ( cors.addCustomCors ( handlerConfig, globalConfig ) );
99
+ }
100
+
101
+ const fastifyMethod = translateLegacyMethods ( method.toLowerCase () );
102
+ const handler = endpoint.handlers[ method ];
103
+ const wrappedHandler = fastifyHandlerWrapper ( handler, endpoint.handlers.config, globalConfig );
104
+
105
+ app[ fastifyMethod ] (
106
+ endpoint.path,
107
+ {
108
+ preHandler: preHandlers
109
+ },
110
+ wrappedHandler
111
+ );
112
+ } );
113
+ }
114
+
115
+ function translateLegacyMethods ( method ) {
116
+ switch ( method ) {
117
+ case 'del':
118
+ return 'delete';
119
+ default:
120
+ return method;
121
+ }
122
+ }
123
+
124
+ function fastifyHandlerWrapper ( handler, config, globalConfig ) {
125
+ return async ( req, reply ) => {
126
+ try {
127
+ let response = await caching.checkRequestCache ( app, req, reply, config, globalConfig );
128
+
129
+ if ( !response ){
130
+ response = await handler ( req );
131
+ await caching.setRequestCache ( app, req, response, config, globalConfig );
132
+ }
133
+
134
+ handleResponse ( reply, response, req.method, req.routeOptions.url );
135
+ }
136
+ catch ( error ) {
137
+ handleError ( reply, error );
138
+ }
139
+ };
140
+ };
141
+
142
+ function handleResponse ( reply, response, method, path ) {
117
143
 
118
- res.send ( new restifyErrors.InternalServerError ( { code: 500 }, JSON.stringify ( error, null, 2 ) ) );
119
- }
144
+ if ( !response ) {
145
+ reply.code ( 204 ).send ();
146
+
147
+ return;
148
+ }
120
149
 
121
- return;
122
- }
123
- } );
150
+ if ( typeof response !== 'object' || response === null ) {
151
+ handleNonObjectResponse ( reply, response, method );
152
+
153
+ return;
154
+ }
155
+
156
+ setResponseHeaders ( reply, response );
157
+ handleSpecialResponseTypes ( reply, response, method, path );
158
+ }
159
+
160
+ function setResponseHeaders ( reply, response ) {
161
+ if ( response.headers ) {
162
+ Object.entries ( response.headers ).forEach ( ( [ key, value ] ) => {
163
+ reply.header ( key, value );
124
164
  } );
165
+ }
166
+ }
167
+
168
+ function handleSpecialResponseTypes ( reply, response, method, path ) {
169
+ if ( response.redirect ) {
170
+ reply.redirect ( 301, response.redirect.url );
171
+
172
+ return;
173
+ }
174
+
175
+ if ( response.html ) {
176
+ reply.code ( 200 ).type ( 'text/html' ).send ( response.html );
177
+
178
+ return;
179
+ }
180
+
181
+ if ( response.text ) {
182
+ reply.code ( 200 ).type ( 'text/plain' ).send ( response.text );
183
+
184
+ return;
185
+ }
186
+
187
+ if ( response.error ) {
188
+ handleErrorResponse ( reply, response.error );
189
+
190
+ return;
191
+ }
192
+
193
+ if ( response.file ) {
194
+ reply.send ( response.file );
195
+
196
+ return;
197
+ }
198
+
199
+ sendGenericResponse ( reply, response, method, path );
200
+ }
201
+
202
+ function sendGenericResponse ( reply, response, method, path ) {
203
+
204
+ const data = response.json ?? response.body ?? response.response ?? response;
205
+
206
+ if ( data !== undefined ) {
207
+ reply.type ( 'application/json' ).code ( method === 'post' ? 201 : 200 ).send ( data );
208
+ }
209
+ else {
210
+ handleUnknownResponseType ( reply, method, path );
211
+ }
212
+
213
+ }
214
+
215
+ function handleErrorResponse ( reply, error ) {
216
+ console.error ( error );
217
+ const statusCode = error?.statusCode ?? 500;
218
+ reply.code ( statusCode ).type ( 'application/json' ).send ( { error: error.message } );
219
+ }
220
+
221
+ function handleUnknownResponseType ( reply, method, path ) {
222
+ reply.type ( 'application/json' ).code ( 500 ).send ( {
223
+ error: `Unrecognized response type for ${method} ${path}`
125
224
  } );
126
-
127
- await server.listen ( process.env.PORT || config?.port || 4001 );
128
-
129
- return server;
130
- };
225
+ }
226
+
227
+ function handleNonObjectResponse ( reply, response, method ) {
228
+ reply.type ( 'text/plain' ).code ( method === 'post' ? 201 : 200 ).send ( response.toString () );
229
+ }
230
+
231
+ function handleError ( reply, error ) {
232
+ // Set content type for the error response
233
+ reply.header ( 'Content-Type', 'application/json' );
234
+
235
+ if ( error instanceof Error ) {
236
+ // Send internal server error with the error stack
237
+ reply.status ( 500 ).send ( {
238
+ error: 'Internal Server Error',
239
+ message: error.message,
240
+ stack: error.stack
241
+ } );
242
+ }
243
+ else {
244
+ // Check if error object contains a specific status code
245
+ if ( error.code && typeof error.code === 'number' ) {
246
+ reply.status ( error.code ).send ( {
247
+ error: 'Error',
248
+ message: error.message
249
+ } );
250
+ }
251
+ else {
252
+ // Send a generic internal server error response
253
+ reply.status ( 500 ).send ( {
254
+ error: 'Internal Server Error',
255
+ message: 'An unknown error occurred'
256
+ } );
257
+ }
258
+ }
259
+ }
131
260
 
132
261
  const handlerPathToApiPath = ( path, docRoot ) => {
133
262
 
@@ -152,28 +281,10 @@ const recursiveReadDir = async ( docRoot ) => {
152
281
  try {
153
282
  const files = await recursiveRead ( docRoot );
154
283
 
155
- // Remove all falsy values and reverse the array.
156
284
  return files.filter ( filePath => filePath ? !filePath.includes ( 'DS_Store' ) : false ).reverse ();
157
285
  }
158
286
  catch ( e ){
159
287
  console.error ( 'JustAnother: Failed to load your routes directory for generating endpoints.' );
160
288
  throw e;
161
289
  }
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
290
  };
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.0",
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
+ };