eip-cloud-services 1.0.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/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Import individual service modules
2
+ exports.redis = require ( './src/redis' );
3
+ exports.s3 = require ( './src/s3' );
4
+ exports.cdn = require ( './src/cdn' );
5
+ exports.lambda = require ( './src/lambda' );
6
+ exports.mysql = require ( './src/mysql' );
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "eip-cloud-services",
3
+ "version": "1.0.0",
4
+ "description": "Houses a collection of helpers for connecting with Cloud services.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha"
8
+ },
9
+ "author": "Oliver Edgington <oliver@edgington.com>",
10
+ "license": "ISC",
11
+ "devDependencies": {
12
+ "chai": "^4.3.7",
13
+ "mocha": "^10.2.0"
14
+ },
15
+ "dependencies": {
16
+ "@aws-sdk/client-cloudfront": "^3.354.0",
17
+ "@aws-sdk/client-lambda": "^3.356.0",
18
+ "@aws-sdk/client-s3": "^3.354.0",
19
+ "@aws-sdk/client-secrets-manager": "^3.354.0",
20
+ "config": "^3.3.9",
21
+ "google-auth-library": "^8.8.0",
22
+ "mysql": "^2.18.1",
23
+ "redis": "^4.6.7"
24
+ }
25
+ }
package/src/cdn.js ADDED
@@ -0,0 +1,86 @@
1
+ const { CloudFrontClient, CreateInvalidationCommand } = require ( '@aws-sdk/client-cloudfront' );
2
+ const { GoogleAuth } = require ( 'google-auth-library' );
3
+ const { initialiseGoogleAuth } = require ( './gcp' );
4
+ const config = require ( 'config' );
5
+ const packageJson = require ( '../package.json' );
6
+
7
+ /**
8
+ * Create a CDN invalidation for the specified key(s) and environment.
9
+ *
10
+ * @param {string} cdn - The CDN provider to be used (e.g., 'google', 'amazon').
11
+ * @param {string|string[]} key - The key(s) representing the file(s) to invalidate.
12
+ * @param {string} environment - The environment (e.g., 'staging', 'production').
13
+ * @returns {Promise<void>} A promise that resolves when the invalidation is created.
14
+ * @description Creates a CDN invalidation for the specified key(s) in the specified environment.
15
+ * - The `cdn` parameter specifies the CDN provider (e.g., 'google', 'amazon').
16
+ * - The `key` parameter can be a single string or an array of strings representing the file(s) to invalidate.
17
+ * - The `environment` parameter specifies the environment (e.g., 'staging', 'production').
18
+ * - The function validates the `key` argument and throws an error if it is not a string or an array of strings.
19
+ * - The function constructs the invalidation paths based on the provided keys.
20
+ * - The CDN client (either Google or Amazon CloudFront) is used to send the invalidation command.
21
+ * - Returns a promise that resolves when the invalidation is created.
22
+ * - The function initializes Google Auth if Google CDN is used.
23
+ * - If Google CDN is used, the function makes a POST request to Google's 'invalidateCache' endpoint for each provided path.
24
+ * - If Amazon CloudFront is used, the function sends an invalidation command to CloudFront.
25
+ * - If the CDN type is not 'google' or 'amazon', the function throws an error.
26
+ * - The function uses the 'config' module to access the CDN settings based on the CDN provider and environment.
27
+ */
28
+ exports.createInvalidation = async ( cdn, key, environment = 'production' ) => {
29
+ const cdnSettings = config.cdn[ cdn ][ environment ];
30
+
31
+ // Ensure paths is an array and sanitize paths
32
+ const paths = ( Array.isArray ( key ) ? key : [ key ] )
33
+ .filter ( item => typeof item === 'string' )
34
+ .map ( item => item.charAt ( 0 ) !== '/' ? '/' + item : item );
35
+
36
+ if ( paths.length === 0 ) {
37
+ throw new Error ( 'Invalid key argument. Expected a string or an array of strings.' );
38
+ }
39
+
40
+ switch ( cdnSettings.type ) {
41
+ case 'google':
42
+ await invalidateGoogleCDN ( cdnSettings, paths );
43
+ break;
44
+
45
+ case 'amazon':
46
+ await invalidateAmazonCDN ( cdnSettings, paths );
47
+ break;
48
+
49
+ default:
50
+ throw new Error ( `Invalid cdn type: ${cdnSettings.type}` );
51
+ }
52
+ };
53
+
54
+ async function invalidateGoogleCDN ( cdnSettings, paths ) {
55
+ await initialiseGoogleAuth ();
56
+
57
+ const auth = new GoogleAuth ( {
58
+ scopes: 'https://www.googleapis.com/auth/cloud-platform'
59
+ } );
60
+
61
+ const client = await auth.getClient ();
62
+ const url = `https://compute.googleapis.com/compute/v1/projects/${cdnSettings.projectId}/global/urlMaps/${cdnSettings.urlMapName}/invalidateCache`;
63
+ const headers = await client.getRequestHeaders ( url );
64
+
65
+ await Promise.all ( paths.map ( path => fetch ( url, {
66
+ method: 'POST',
67
+ headers,
68
+ body: JSON.stringify ( { path } )
69
+ } ) ) );
70
+ }
71
+
72
+ async function invalidateAmazonCDN ( cdnSettings, paths ) {
73
+ const client = new CloudFrontClient ();
74
+ const command = new CreateInvalidationCommand ( {
75
+ DistributionId: cdnSettings.distributionId,
76
+ InvalidationBatch: {
77
+ CallerReference: `${packageJson.name}-${Date.now ()}`,
78
+ Paths: {
79
+ Quantity: paths.length,
80
+ Items: paths,
81
+ },
82
+ },
83
+ } );
84
+
85
+ await client.send ( command );
86
+ }
package/src/gcp.js ADDED
@@ -0,0 +1,41 @@
1
+ const { SecretsManagerClient, GetSecretValueCommand } = require ( '@aws-sdk/client-secrets-manager' );
2
+ const { writeFile, readFile } = require ( 'fs/promises' );
3
+ const os = require ( 'os' );
4
+
5
+ exports.initialiseGoogleAuth = async () => {
6
+
7
+ if ( !process.env.GOOGLE_APPLICATION_CREDENTIALS ){
8
+
9
+ try {
10
+ await readFile ( `${os.tmpdir ()}/gcp.json` );
11
+ process.env.GOOGLE_APPLICATION_CREDENTIALS = './gcp.json';
12
+
13
+ return;
14
+ }
15
+ catch ( error ){
16
+ //file doesnt exists, so continue to create it below.
17
+ }
18
+
19
+ const secret_name = 'gcp-tmg-product-innovation-prod-all-access';
20
+ const client = new SecretsManagerClient ( { region: 'eu-west-1' } );
21
+
22
+ let response;
23
+
24
+ try {
25
+ response = await client.send (
26
+ new GetSecretValueCommand ( {
27
+ SecretId: secret_name,
28
+ VersionStage: 'AWSCURRENT'
29
+ } )
30
+ );
31
+ }
32
+ catch ( error ) {
33
+ throw error;
34
+ }
35
+
36
+ await writeFile ( `${os.tmpdir ()}/gcp.json`, response.SecretString );
37
+ process.env.GOOGLE_APPLICATION_CREDENTIALS = `${os.tmpdir ()}/gcp.json`;
38
+ }
39
+
40
+ return;
41
+ };
package/src/lambda.js ADDED
@@ -0,0 +1,49 @@
1
+ const { LambdaClient, InvokeCommand } = require ( '@aws-sdk/client-lambda' );
2
+ const config = require ( 'config' );
3
+ const packageJson = require ( '../package.json' );
4
+
5
+ /**
6
+ * Invokes an AWS Lambda function.
7
+ *
8
+ * @param {string} functionName - The name of the function you want to invoke.
9
+ * @param {Object} eventPayload - The payload of an event to pass to the function.
10
+ * @param {boolean} [waitForExecution=false] - Flag to indicate whether to wait for the execution of the function (default: false).
11
+ * @param {string} [context=null] - The client context for the function execution.
12
+ * @param {string} [invocationType='Event'] - The invocation type for the function (default: 'Event').
13
+ * @param {string} [logType='None'] - The type of logging for the function (default: 'None').
14
+ * @returns {Promise<any> | null} - A promise that resolves with the response of the function invocation if `waitForExecution` is true, otherwise null.
15
+ *
16
+ * @description
17
+ * This function invokes an AWS Lambda function using the provided parameters. It allows you to specify the name of the function,
18
+ * the payload to pass as an event, and configure optional parameters such as whether to wait for the execution to complete,
19
+ * the client context, invocation type, and log type.
20
+ *
21
+ * If `waitForExecution` is set to `true`, the function will await the response from the invocation and return it as a promise.
22
+ * If `waitForExecution` is `false`, the function will initiate the invocation but not wait for the response, returning null.
23
+ *
24
+ * Note: The `invokeLambda` function relies on the `config` module to obtain the client context. Make sure to have the necessary configuration in place for successful execution.
25
+ */
26
+ exports.invokeLambda = async ( functionName, eventPayload, waitForExecution = false, context = null, invocationType = 'Event', logType = 'None' ) => {
27
+ const params = {
28
+ FunctionName: functionName,
29
+ ClientContext: context ? context : packageJson.name,
30
+ InvocationType: invocationType,
31
+ LogType: logType,
32
+ Payload: eventPayload ? JSON.stringify ( eventPayload ) : undefined /* Strings will be Base-64 encoded on your behalf */
33
+ };
34
+
35
+ const client = new LambdaClient ();
36
+ const command = new InvokeCommand ( params );
37
+ console.log ( `[LAMBDA][START] New invoke to function: ${functionName}` );
38
+
39
+ if ( waitForExecution ) {
40
+ const response = await client.send ( command );
41
+
42
+ return response;
43
+ }
44
+ else {
45
+ client.send ( command );
46
+
47
+ return null;
48
+ }
49
+ };
package/src/mysql.js ADDED
@@ -0,0 +1,53 @@
1
+ const mysql = require ( 'mysql' );
2
+ const config = require ( 'config' );
3
+
4
+ const pool = mysql.createPool (
5
+ {
6
+ connectionLimit: config.mysql.connectionLimit,
7
+ host: config.mysql.host,
8
+ user: config.mysql.user,
9
+ password: config.mysql.password,
10
+ database: config.mysql.database,
11
+ multipleStatements: true
12
+ }
13
+ );
14
+
15
+ const newQuery = ( connection, query ) => new Promise ( ( resolve, reject ) => {
16
+ connection.query ( query, ( error, results, fields ) => {
17
+ connection.release ();
18
+ if ( error ) {
19
+ console.error ( error );
20
+ console.error ( `There was a problem with query: ${ query }` );
21
+ reject ( `There was a problem with query: ${ query }` );
22
+ }
23
+ else {
24
+ resolve ( results );
25
+ }
26
+ } );
27
+ } );
28
+
29
+ const getConnection = () => new Promise ( ( resolve, reject ) => {
30
+ pool.getConnection ( ( error, connection ) => {
31
+ if ( error ) {
32
+ console.log ( error );
33
+ console.error ( 'There was a problem getting a new database connection.' );
34
+ reject ( 'There was a problem getting a new database connection.' );
35
+ }
36
+ else {
37
+ resolve ( connection );
38
+ }
39
+ } );
40
+ } );
41
+
42
+ exports.query = queryString => new Promise ( ( resolve, reject ) => {
43
+ if ( !queryString.endsWith ( ';' ) ){
44
+ queryString += ';';
45
+ }
46
+ getConnection ().then ( connection => newQuery ( connection, queryString ) ).then ( resolve ).catch ( reject );
47
+ } );
48
+
49
+ exports.kill = () => new Promise ( resolve => {
50
+ pool.end ( () => {
51
+ resolve ();
52
+ } );
53
+ } );
package/src/redis.js ADDED
@@ -0,0 +1,496 @@
1
+ const redis = require ( 'redis' );
2
+ const config = require ( 'config' );
3
+ let redisClient;
4
+
5
+ const getClient = async () => {
6
+ try {
7
+ if ( !redisClient ) {
8
+ redisClient = redis.createClient ( { url: `${config.redis.host}:${config.redis.port}` } );
9
+ await redisClient.connect ();
10
+ redisClient.on ( 'error', error => {
11
+ console.error ( '\x1b[33mREDIS CONNECTION FAILED: Redis connection failed. If you\'re running locally, is a redis server actually active? Also, check your connection configuration.\x1b[0m' );
12
+
13
+ throw error;
14
+ }, { once: true } );
15
+ }
16
+
17
+ return redisClient;
18
+ }
19
+ catch ( error ){
20
+ throw new Error ( 'Redis connection failed. If running locally is redis actually running? Also check your connection configuration.' );
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Retrieves all keys matching a pattern.
26
+ * @param {string} pattern - The pattern to match.
27
+ * @returns {Promise} - A promise that resolves with the keys matching the pattern.
28
+ *
29
+ * @description This method retrieves all keys from Redis that match the specified pattern.
30
+ * The pattern can include wildcards (* and ?) to perform pattern matching.
31
+ */
32
+ exports.keys = async ( pattern ) => {
33
+ try {
34
+ if ( typeof pattern === 'string' ) {
35
+ const client = await getClient ();
36
+
37
+ const commandArgs = [
38
+ pattern.startsWith ( config.redis.prefix ) ? pattern : config.redis.prefix + pattern,
39
+ ];
40
+
41
+ return await client.sendCommand ( [ 'KEYS', ...commandArgs ] );
42
+ }
43
+
44
+ throw new Error ( `redis.keys expects a string pattern, instead the type provided was: ${typeof pattern}` );
45
+ }
46
+ catch ( error ) {
47
+ console.log ( 'REDIS LIB ERROR [keys]', error );
48
+ throw error;
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Retrieves the value of a key.
54
+ * @param {string} key - The key to retrieve.
55
+ * @returns {Promise} - A promise that resolves with the value of the key.
56
+ *
57
+ * @description This method retrieves the value associated with the specified key from Redis.
58
+ * If the value is a JSON string, it will be parsed and returned as a JavaScript object.
59
+ * If the value is not a valid JSON string, it will be returned as a string.
60
+ */
61
+ exports.get = async ( key ) => {
62
+ try {
63
+ if ( typeof key === 'string' ) {
64
+ const client = await getClient ();
65
+
66
+ const commandArgs = [
67
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
68
+ ];
69
+
70
+ const data = await client.sendCommand ( [ 'GET', ...commandArgs ] );
71
+
72
+ try {
73
+ return JSON.parse ( data );
74
+ }
75
+ catch {
76
+ return data;
77
+ }
78
+ }
79
+
80
+ throw new Error ( `redis.get expects a string key, instead the type provided was: ${typeof key}` );
81
+ }
82
+ catch ( error ) {
83
+ console.log ( 'REDIS LIB ERROR [get]', error.stack );
84
+ throw error;
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Sets the value of a key.
90
+ * @param {string} key - The key to set.
91
+ * @param {*} value - The value to set for the key.
92
+ * @param {string} [ex] - Optional EX parameter to set expiration in seconds.
93
+ * @param {number} [px] - Optional PX parameter to set expiration in milliseconds.
94
+ * @returns {Promise} - A promise that resolves with the result of the SET command.
95
+ *
96
+ * @description This method sets the value for the specified key in Redis.
97
+ * If the value is an object or array, it will be automatically converted to a JSON string before storing.
98
+ * Optionally, you can set an expiration time for the key using the `ex` parameter in seconds or the `px` parameter in milliseconds.
99
+ */
100
+ exports.set = async ( key, value, ex, px ) => {
101
+ try {
102
+ if ( typeof key === 'string' ) {
103
+ const client = await getClient ();
104
+
105
+ const commandArgs = [
106
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
107
+ value,
108
+ ];
109
+
110
+ if ( ex ) commandArgs.push ( 'EX', String ( ex ) );
111
+ if ( px ) commandArgs.push ( 'PX', String ( px ) );
112
+
113
+ return await client.sendCommand ( [ 'SET', ...commandArgs ] );
114
+ }
115
+
116
+ throw new Error ( `redis.set expects a string key, instead the type provided was: ${typeof key}` );
117
+ }
118
+ catch ( error ) {
119
+ console.log ( 'REDIS LIB ERROR [set]', error );
120
+ throw error;
121
+ }
122
+ };
123
+
124
+ /**
125
+ * Deletes a key.
126
+ * @param {string} key - The key to delete.
127
+ * @returns {Promise} - A promise that resolves with the result of the DEL command.
128
+ *
129
+ * @description This method deletes the specified key from Redis.
130
+ * If the key does not exist, it will simply return 0 indicating no deletion occurred.
131
+ * If the key is successfully deleted, it will return 1 indicating one key was deleted.
132
+ */
133
+ exports.del = async ( key ) => {
134
+ try {
135
+ if ( typeof key === 'string' ) {
136
+ const client = await getClient ();
137
+
138
+ const commandArgs = [
139
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
140
+ ];
141
+
142
+ return await client.sendCommand ( [ 'DEL', ...commandArgs ] );
143
+ }
144
+
145
+ throw new Error ( `redis.del expects a string key, instead the type provided was: ${typeof key}` );
146
+ }
147
+ catch ( error ) {
148
+ console.log ( 'REDIS LIB ERROR [del]', error.stack );
149
+ throw error;
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Adds one or more members to a set.
155
+ * @param {string} key - The key of the set.
156
+ * @param {string|string[]} members - The member(s) to add to the set.
157
+ * @returns {Promise} - A promise that resolves with the result of the SADD command.
158
+ *
159
+ * @description This method adds one or more members to the specified set in Redis.
160
+ * If the set does not exist, it will be created.
161
+ * If any member already exists in the set, it will be ignored.
162
+ * It returns the number of members that were added to the set.
163
+ */
164
+ exports.setAdd = async ( key, members ) => {
165
+ try {
166
+ if ( typeof key === 'string' ) {
167
+ const client = await getClient ();
168
+
169
+ const commandArgs = [
170
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
171
+ ...( Array.isArray ( members ) ? members : [ members ] ),
172
+ ];
173
+
174
+ return await client.sendCommand ( [ 'SADD', ...commandArgs ] );
175
+ }
176
+
177
+ throw new Error ( `redis.setAdd expects a string key, instead the type provided was: ${typeof key}` );
178
+ }
179
+ catch ( error ) {
180
+ console.log ( 'REDIS LIB ERROR [setAdd]', error );
181
+ throw error;
182
+ }
183
+ };
184
+
185
+ /**
186
+ * Removes one or more members from a set.
187
+ * @param {string} key - The key of the set.
188
+ * @param {string|string[]} members - The member(s) to remove from the set.
189
+ * @returns {Promise} - A promise that resolves with the result of the SREM command.
190
+ *
191
+ * @description This method removes one or more members from the specified set in Redis.
192
+ * If the set does not exist or if any member does not exist in the set, it will be ignored.
193
+ * It returns the number of members that were removed from the set.
194
+ */
195
+ exports.setRemove = async ( key, members ) => {
196
+ try {
197
+ if ( typeof key === 'string' ) {
198
+ const client = await getClient ();
199
+
200
+ const commandArgs = [
201
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
202
+ ...( Array.isArray ( members ) ? members : [ members ] ),
203
+ ];
204
+
205
+ return await client.sendCommand ( [ 'SREM', ...commandArgs ] );
206
+ }
207
+
208
+ throw new Error ( `redis.setRemove expects a string key, instead the type provided was: ${typeof key}` );
209
+ }
210
+ catch ( error ) {
211
+ console.log ( 'REDIS LIB ERROR [setRemove]', error.stack );
212
+ throw error;
213
+ }
214
+ };
215
+
216
+ /**
217
+ * Retrieves all members of a set.
218
+ * @param {string} key - The key of the set.
219
+ * @returns {Promise} - A promise that resolves with the result of the SMEMBERS command.
220
+ *
221
+ * @description This method retrieves all members of the specified set in Redis.
222
+ * If the set does not exist, it will return an empty array.
223
+ */
224
+ exports.setMembers = async ( key ) => {
225
+ try {
226
+ if ( typeof key === 'string' ) {
227
+ const client = await getClient ();
228
+
229
+ const commandArgs = [
230
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
231
+ ];
232
+
233
+ return await client.sendCommand ( [ 'SMEMBERS', ...commandArgs ] );
234
+ }
235
+
236
+ throw new Error ( `redis.setMembers expects a string key, instead the type provided was: ${typeof key}` );
237
+ }
238
+ catch ( error ) {
239
+ console.log ( 'REDIS LIB ERROR [setMembers]', error.stack );
240
+ throw error;
241
+ }
242
+ };
243
+
244
+ /**
245
+ * Removes and returns a random member from a set.
246
+ * @param {string} key - The key of the set.
247
+ * @returns {Promise} - A promise that resolves with the removed member, or null if the set is empty.
248
+ *
249
+ * @description
250
+ * This function removes and returns a random member from a set identified by the provided key.
251
+ * It uses the SPOP command in Redis to perform the operation.
252
+ *
253
+ * @throws {Error} If the key parameter is not a string.
254
+ */
255
+ exports.setPop = async ( key ) => {
256
+ try {
257
+ if ( typeof key === 'string' ) {
258
+ const client = await getClient ();
259
+
260
+ const commandArgs = [
261
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
262
+ ];
263
+
264
+ const member = await client.sendCommand ( [ 'SPOP', ...commandArgs ] );
265
+
266
+ // If the member is null or an empty string, return null
267
+ if ( member === null || member === '' ) {
268
+ return null;
269
+ }
270
+
271
+ return member;
272
+ }
273
+
274
+ throw new Error ( `redis.setPop expects a string key, instead the type provided was: ${typeof key}` );
275
+ }
276
+ catch ( error ) {
277
+ console.log ( 'REDIS LIB ERROR [setPop]', error.stack );
278
+ throw error;
279
+ }
280
+ };
281
+
282
+ /**
283
+ * Prepends multiple values to the head of a list.
284
+ * @param {string} key - The key of the list.
285
+ * @param {string|string[]} values - The values to prepend to the list.
286
+ * @returns {Promise} - A promise that resolves with the result of the LPUSH command.
287
+ *
288
+ * @description This method prepends one or more values to the head of the specified list in Redis.
289
+ * If the list does not exist, it will be created.
290
+ * The values are inserted in the same order as they are provided.
291
+ * It returns the length of the list after the prepend operation.
292
+ */
293
+ exports.listUnshift = async ( key, values ) => {
294
+ try {
295
+ if ( typeof key === 'string' ) {
296
+ const client = await getClient ();
297
+
298
+ const commandArgs = [
299
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
300
+ ];
301
+
302
+ if ( typeof values === 'string' ) {
303
+ commandArgs.push ( values );
304
+ }
305
+ else if ( Array.isArray ( values ) ) {
306
+ commandArgs.push ( ...values );
307
+ }
308
+ else {
309
+ throw new Error ( `redis.listUnshift expects a string or an array of strings as the values, instead the type provided was: ${typeof values}` );
310
+ }
311
+
312
+ return await client.sendCommand ( [ 'LPUSH', ...commandArgs ] );
313
+ }
314
+
315
+ throw new Error ( `redis.listUnshift expects a string key, instead the type provided was: ${typeof key}` );
316
+ }
317
+ catch ( error ) {
318
+ console.log ( 'REDIS LIB ERROR [listUnshift]', error.stack );
319
+ throw error;
320
+ }
321
+ };
322
+
323
+ /**
324
+ * Pushes values to the tail of a list.
325
+ * @param {string} key - The key of the list.
326
+ * @param {string|string[]} values - The value(s) to push to the list.
327
+ * @returns {Promise} - A promise that resolves with the result of the RPUSH command.
328
+ *
329
+ * @description This method pushes one or more values to the tail of the specified list in Redis.
330
+ * If the list does not exist, it will be created.
331
+ * The values are inserted in the same order as they are provided.
332
+ * It returns the length of the list after the push operation.
333
+ */
334
+ exports.listPush = async ( key, values ) => {
335
+ try {
336
+ if ( typeof key === 'string' ) {
337
+ const client = await getClient ();
338
+
339
+ const commandArgs = [
340
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
341
+ ];
342
+
343
+ if ( typeof values === 'string' ) {
344
+ commandArgs.push ( values );
345
+ }
346
+ else if ( Array.isArray ( values ) ) {
347
+ commandArgs.push ( ...values );
348
+ }
349
+ else {
350
+ throw new Error ( `redis.listPush expects a string or an array of strings as the values, instead the type provided was: ${typeof values}` );
351
+ }
352
+
353
+ return await client.sendCommand ( [ 'RPUSH', ...commandArgs ] );
354
+ }
355
+
356
+ throw new Error ( `redis.listPush expects a string key, instead the type provided was: ${typeof key}` );
357
+ }
358
+ catch ( error ) {
359
+ console.log ( 'REDIS LIB ERROR [listPush]', error.stack );
360
+ throw error;
361
+ }
362
+ };
363
+
364
+ /**
365
+ * Removes and returns the first element of a list.
366
+ * @param {string} key - The key of the list.
367
+ * @param {number} count - The number of items to remove (default: 0).
368
+ * @returns {Promise} - A promise that resolves with the result of the LPOP command.
369
+ *
370
+ * @description This method removes and returns the first element(s) from the specified list in Redis.
371
+ * If the list is empty or does not exist, it will return null.
372
+ * If the count parameter is provided, it specifies the number of items to remove.
373
+ * If count is greater than the number of items in the list, it will remove all items.
374
+ */
375
+ exports.listPop = async ( key, count = 1 ) => {
376
+ try {
377
+ if ( typeof key === 'string' ) {
378
+ const client = await getClient ();
379
+
380
+ const commandArgs = [
381
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
382
+ String ( count ),
383
+ ];
384
+
385
+ return await client.sendCommand ( [ 'LPOP', ...commandArgs ] );
386
+ }
387
+
388
+ throw new Error ( `redis.listPop expects a string key, instead the type provided was: ${typeof key}` );
389
+ }
390
+ catch ( error ) {
391
+ console.log ( 'REDIS LIB ERROR [listPop]', error.stack );
392
+ throw error;
393
+ }
394
+ };
395
+
396
+ /**
397
+ * Retrieves a range of elements from a list.
398
+ * @param {string} key - The key of the list.
399
+ * @param {number} start - The starting index (default: 0).
400
+ * @param {number} end - The ending index (default: -1).
401
+ * @returns {Promise} - A promise that resolves with the result of the LRANGE command.
402
+ *
403
+ * @description This method retrieves a range of elements from the specified list in Redis.
404
+ * The range is specified by the start and end indices.
405
+ * If the list is empty or does not exist, it will return an empty array.
406
+ * If the end index is omitted or set to -1, it will include all elements from the start index to the end of the list.
407
+ */
408
+ exports.listRange = async ( key, start = 0, end = -1 ) => {
409
+ try {
410
+ if ( typeof key === 'string' ) {
411
+ const client = await getClient ();
412
+
413
+ const commandArgs = [
414
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
415
+ String ( start ),
416
+ String ( end ),
417
+ ];
418
+
419
+ return await client.sendCommand ( [ 'LRANGE', ...commandArgs ] );
420
+ }
421
+
422
+ throw new Error ( `redis.listRange expects a string key, instead the type provided was: ${typeof key}` );
423
+ }
424
+ catch ( error ) {
425
+ console.log ( 'REDIS LIB ERROR [listRange]', error.stack );
426
+ throw error;
427
+ }
428
+ };
429
+
430
+ /**
431
+ * Retrieves the expiration time of a key in seconds.
432
+ * @param {string} key - The key to get the expiration time of.
433
+ * @returns {Promise} - A promise that resolves with the result of the TTL command.
434
+ *
435
+ * @description This method retrieves the expiration time (in seconds) of the specified key in Redis.
436
+ * If the key does not exist or does not have an expiration set, it will return -1.
437
+ * If the key exists but has no associated expiration time, it will return -1.
438
+ * If the key exists and has an expiration time, it will return the number of seconds remaining until the key expires.
439
+ */
440
+ exports.getExpiryInSeconds = async ( key ) => {
441
+ try {
442
+ if ( typeof key === 'string' ) {
443
+ const client = await getClient ();
444
+
445
+ const commandArgs = [
446
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
447
+ ];
448
+
449
+ return await client.sendCommand ( [ 'TTL', ...commandArgs ] );
450
+ }
451
+
452
+ throw new Error ( `redis.getExpiryInSeconds expects a string key, instead the type provided was: ${typeof key}` );
453
+ }
454
+ catch ( error ) {
455
+ console.log ( 'REDIS LIB ERROR [getExpiryInSeconds]', error.stack );
456
+ throw error;
457
+ }
458
+ };
459
+
460
+ /**
461
+ * Sets the expiration time for a key in seconds.
462
+ * @param {string} key - The key to set the expiration time for.
463
+ * @param {number} seconds - The expiration time in seconds (default: 0).
464
+ * @returns {Promise} - A promise that resolves with the result of the EXPIRE command.
465
+ *
466
+ * @description This method sets the expiration time (in seconds) for the specified key in Redis.
467
+ * If the key does not exist, it will return 0 indicating no expiration was set.
468
+ * If the key is successfully set to expire, it will return 1 indicating the expiration was set.
469
+ */
470
+ exports.setExpiry = async ( key, seconds = 0 ) => {
471
+ try {
472
+ if ( typeof key === 'string' ) {
473
+ const client = await getClient ();
474
+
475
+ const commandArgs = [
476
+ key.startsWith ( config.redis.prefix ) ? key : config.redis.prefix + key,
477
+ String ( seconds ),
478
+ ];
479
+
480
+ return await client.sendCommand ( [ 'EXPIRE', ...commandArgs ] );
481
+ }
482
+
483
+ throw new Error ( `redis.setExpiry expects a string key, instead the type provided was: ${typeof key}` );
484
+ }
485
+ catch ( error ) {
486
+ console.log ( 'REDIS LIB ERROR [setExpiry]', error.stack );
487
+ throw error;
488
+ }
489
+ };
490
+
491
+ exports.kill = async () => {
492
+ const client = await getClient ();
493
+ client.quit ();
494
+
495
+ return;
496
+ };
package/src/s3.js ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Helpful Information
3
+ * ----------------------------------------------------------------
4
+ *
5
+ * Cache-Control Directives
6
+ *
7
+ * The Cache-Control header contains directives that control the caching behavior
8
+ * of a resource in both client and proxy caches. These directives provide fine-grained
9
+ * control over how and when caches should store and serve the resource. Understanding
10
+ * these directives is important for optimizing caching strategies and ensuring the
11
+ * proper handling of cached content.
12
+ *
13
+ * - max-age: Specifies the maximum age of the resource in seconds.
14
+ * - s-maxage: Specifies the maximum age of the resource in shared caches (e.g., CDNs e.g., CloudFront).
15
+ * - public: Indicates that the response can be cached by both public and private caches.
16
+ * - private: Indicates that the response is intended for a single user and should not be
17
+ * cached by shared caches.
18
+ * - no-cache: Requires the cache to revalidate the resource with the server before
19
+ * serving it. The resource may still be cached, but the cache must check with the server
20
+ * for any updates.
21
+ * - no-store: Instructs caches not to store the response under any circumstances.
22
+ * - must-revalidate: Requires the cache to revalidate the resource with the server before
23
+ * serving it to subsequent requests. If the resource is expired, the cache must send a
24
+ * conditional request to the server for revalidation.
25
+ * - proxy-revalidate: Similar to must-revalidate, but specifically applies to proxy caches.
26
+ * Instructs proxy caches to revalidate the resource with the server, even if it has
27
+ * previously been validated and marked as fresh.
28
+ * - no-transform: Instructs intermediaries not to modify the response, such as by
29
+ * transforming the content encoding or media type.
30
+ * - immutable: Indicates that the resource is considered immutable and should not change.
31
+ * Caches can store immutable resources indefinitely.
32
+ *
33
+ * It's important to carefully consider the appropriate combination of Cache-Control
34
+ * directives based on your caching requirements and the desired behavior of your
35
+ * client and proxy caches.
36
+ *
37
+ * Taking care of cache-control directives will ensure cloud infrastructure costs are kept low
38
+ * and ensures the best CWVs performance.
39
+ */
40
+
41
+ const { S3Client, HeadObjectCommand, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand, DeleteObjectsCommand } = require ( '@aws-sdk/client-s3' );
42
+ const config = require ( 'config' );
43
+ const zlib = require ( 'zlib' );
44
+
45
+ const S3 = new S3Client ( { region: 'eu-west-1' } );
46
+
47
+ /**
48
+ * Check if an object exists in S3.
49
+ *
50
+ * @param {string} key - The object key.
51
+ * @param {string} [bucket=config.s3.Bucket] - The bucket name. Defaults to the configured bucket.
52
+ * @returns {Promise<boolean>} A promise that resolves to true if the object exists, false otherwise.
53
+ * @description Checks if an object with the specified key exists in S3.
54
+ */
55
+ exports.exists = async ( key, bucket = config.s3.Bucket ) => {
56
+ try {
57
+ const command = new HeadObjectCommand ( {
58
+ Bucket: bucket,
59
+ Key: key
60
+ } );
61
+
62
+ await S3.send ( command );
63
+
64
+ return true;
65
+ }
66
+ catch ( error ) {
67
+ return false;
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Get an object from S3.
73
+ *
74
+ * @param {string} key - The object key.
75
+ * @returns {Promise} A promise that resolves to the retrieved object.
76
+ * @description Retrieves an object from S3 based on the provided key.
77
+ */
78
+ exports.get = async ( key ) => {
79
+ try {
80
+ const command = new GetObjectCommand ( {
81
+ Bucket: config.s3.Bucket,
82
+ Key: key
83
+ } );
84
+
85
+ const response = await S3.send ( command );
86
+ let data = await streamToBuffer ( response.Body );
87
+
88
+ if ( response.ContentEncoding && response.ContentEncoding === 'gzip' ) {
89
+ data = zlib.unzipSync ( data );
90
+ }
91
+
92
+ if ( response.ContentType !== 'application/json' ) {
93
+ return data.toString ( 'utf8' );
94
+ }
95
+
96
+ return JSON.parse ( data.toString ( 'utf8' ) );
97
+ }
98
+ catch ( error ) {
99
+ if ( error.Code === 'NoSuchKey' ) {
100
+ return null;
101
+ }
102
+
103
+ throw error;
104
+ }
105
+ };
106
+
107
+ /**
108
+ * Set an object in S3.
109
+ *
110
+ * @param {string} key - The object key.
111
+ * @param {Buffer|Uint8Array|Blob|string} body - The object body.
112
+ * @param {object} [options] - The optional parameters for setting the object.
113
+ * @param {string} [options.bucket=config.s3.Bucket] - The bucket name. Defaults to the configured bucket.
114
+ * @param {string} [options.contentType='application/json'] - The content type of the object. Defaults to 'application/json'.
115
+ * @param {string} [options.acl='public-read'] - The ACL (Access Control List) of the object. Defaults to 'public-read'.
116
+ * @param {string} [options.cacheControl='max-age=25,s-maxage=30,must-revalidate'] - Sets cache control for the object.
117
+ * @returns {Promise} A promise that resolves when the object is successfully set in S3.
118
+ * @description Sets an object in S3 with the provided key, body, and optional parameters.
119
+ */
120
+ exports.set = async ( key, body, options = {} ) => {
121
+ const {
122
+ bucket = config.s3.Bucket,
123
+ contentType = 'application/json',
124
+ acl = 'public-read',
125
+ cacheControl = 'max-age=25,s-maxage=30,must-revalidate'
126
+ } = options;
127
+
128
+ try {
129
+ const command = new PutObjectCommand ( {
130
+ Bucket: bucket,
131
+ Key: key,
132
+ Body: body,
133
+ ContentType: contentType,
134
+ ACL: acl,
135
+ CacheControl: cacheControl
136
+ } );
137
+
138
+ const data = await S3.send ( command );
139
+
140
+ return data;
141
+ }
142
+ catch ( error ) {
143
+ throw error;
144
+ }
145
+ };
146
+
147
+ /**
148
+ * Delete an object from S3.
149
+ *
150
+ * @param {string} key - The object key.
151
+ * @param {string} [bucket=config.s3.Bucket] - The bucket name. Defaults to the configured bucket.
152
+ * @returns {Promise} A promise that resolves when the object is successfully deleted from S3.
153
+ * @description Deletes an object from S3 based on the provided key.
154
+ */
155
+ exports.del = async ( key, bucket = config.s3.Bucket ) => {
156
+ try {
157
+ const command = new DeleteObjectCommand ( {
158
+ Bucket: bucket,
159
+ Key: key
160
+ } );
161
+
162
+ const data = await S3.send ( command );
163
+
164
+ return data;
165
+ }
166
+ catch ( error ) {
167
+ throw error;
168
+ }
169
+ };
170
+
171
+ /**
172
+ * Move an object within S3 to a different location.
173
+ *
174
+ * @param {string} sourceKey - The source object key.
175
+ * @param {string} destinationKey - The destination object key.
176
+ * @param {string} [sourceBucket=config.s3.Bucket] - The source bucket name. Defaults to the configured bucket.
177
+ * @param {string} [destinationBucket=config.s3.Bucket] - The destination bucket name. Defaults to the configured bucket.
178
+ * @returns {Promise} A promise that resolves when the object is successfully moved in S3.
179
+ * @description Moves an object from the source location to the destination location within S3.
180
+ */
181
+ exports.move = async ( sourceKey, destinationKey, sourceBucket = config.s3.Bucket, destinationBucket = config.s3.Bucket ) => {
182
+ try {
183
+ // Copy the object to the destination location
184
+ const copyCommand = new CopyObjectCommand ( {
185
+ CopySource: `/${sourceBucket}/${sourceKey}`,
186
+ Bucket: destinationBucket,
187
+ Key: destinationKey
188
+ } );
189
+
190
+ await S3.send ( copyCommand );
191
+
192
+ // Delete the object from the source location
193
+ const deleteCommand = new DeleteObjectsCommand ( {
194
+ Bucket: sourceBucket,
195
+ Delete: {
196
+ Objects: [
197
+ { Key: sourceKey }
198
+ ],
199
+ Quiet: true
200
+ }
201
+ } );
202
+
203
+ await S3.send ( deleteCommand );
204
+ }
205
+ catch ( error ) {
206
+ throw error;
207
+ }
208
+ };
209
+
210
+ const streamToBuffer = ( stream ) =>
211
+ new Promise ( ( resolve, reject ) => {
212
+ const chunks = [];
213
+ stream.on ( 'data', ( chunk ) => chunks.push ( chunk ) );
214
+ stream.on ( 'error', reject );
215
+ stream.on ( 'end', () => resolve ( Buffer.concat ( chunks ) ) );
216
+ } );