516 lines
15 KiB
JavaScript
516 lines
15 KiB
JavaScript
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
|
const devDebugLogger = require("./devDebugLogger");
|
|
const client = require("../graphql-client/graphql-client").client;
|
|
|
|
/**
|
|
* Bodyshop cache TTL in seconds
|
|
* @type {number}
|
|
*/
|
|
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
|
|
|
|
/**
|
|
* Chatter API token cache TTL in seconds
|
|
* @type {number}
|
|
*/
|
|
const CHATTER_TOKEN_CACHE_TTL = 3600; // 1 hour
|
|
|
|
/**
|
|
* Generate a cache key for a bodyshop
|
|
* @param bodyshopId
|
|
* @returns {`bodyshop-cache:${string}`}
|
|
*/
|
|
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
|
|
|
|
/**
|
|
* Generate a cache key for a Chatter API token
|
|
* @param companyId
|
|
* @returns {`chatter-token:${string}`}
|
|
*/
|
|
const getChatterTokenCacheKey = (companyId) => `chatter-token:${companyId}`;
|
|
|
|
/**
|
|
* Generate a cache key for a user socket mapping
|
|
* @param email
|
|
* @returns {`user:${string}:${string}:socketMapping`}
|
|
*/
|
|
const getUserSocketMappingKey = (email) =>
|
|
`user:${process.env?.NODE_ENV === "production" ? "prod" : "dev"}:${email}:socketMapping`;
|
|
|
|
const getSocketTransactionkey = ({ socketId, transactionType }) => `socket:${socketId}:${transactionType}`;
|
|
/**
|
|
* Fetch bodyshop data from the database
|
|
* @param bodyshopId
|
|
* @param logger
|
|
* @returns {Promise<*>}
|
|
*/
|
|
const fetchBodyshopFromDB = async (bodyshopId, logger) => {
|
|
try {
|
|
const response = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
|
const bodyshop = response.bodyshops_by_pk;
|
|
if (!bodyshop) {
|
|
throw new Error(`Bodyshop with ID ${bodyshopId} not found`);
|
|
}
|
|
return bodyshop; // Return the full object as-is
|
|
} catch (error) {
|
|
logger.log("fetch-bodyshop-from-db", "ERROR", "redis", null, {
|
|
bodyshopId,
|
|
error: error?.message,
|
|
stack: error?.stack
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Apply Redis helper functions
|
|
* @param pubClient
|
|
* @param app
|
|
* @param logger
|
|
*/
|
|
const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
|
const toRedisJson = (value) => JSON.stringify(value === undefined ? null : value);
|
|
|
|
// Store session data in Redis
|
|
const setSessionData = async (socketId, key, value, ttl) => {
|
|
try {
|
|
const sessionKey = `socket:${socketId}`;
|
|
|
|
// Supports both forms:
|
|
// 1) setSessionData(socketId, "field", value, ttl)
|
|
// 2) setSessionData(socketId, { fieldA: valueA, fieldB: valueB }, ttl)
|
|
if (key && typeof key === "object" && !Array.isArray(key)) {
|
|
const entries = Object.entries(key).flatMap(([field, fieldValue]) => [field, toRedisJson(fieldValue)]);
|
|
|
|
if (entries.length > 0) {
|
|
await pubClient.hset(sessionKey, ...entries);
|
|
}
|
|
|
|
const objectTtl = typeof value === "number" ? value : typeof ttl === "number" ? ttl : null;
|
|
if (objectTtl) {
|
|
await pubClient.expire(sessionKey, objectTtl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
await pubClient.hset(sessionKey, key, toRedisJson(value)); // Use Redis pubClient
|
|
if (ttl && typeof ttl === "number") {
|
|
await pubClient.expire(sessionKey, ttl);
|
|
}
|
|
} catch (error) {
|
|
logger.log(`Error Setting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrieve session data from Redis
|
|
* @param socketId
|
|
* @param key
|
|
* @returns {Promise<any|null>}
|
|
*/
|
|
const getSessionData = async (socketId, key) => {
|
|
try {
|
|
const sessionKey = `socket:${socketId}`;
|
|
|
|
// Supports:
|
|
// 1) getSessionData(socketId, "field") -> parsed field value
|
|
// 2) getSessionData(socketId) -> parsed object of all fields
|
|
if (typeof key === "undefined") {
|
|
const raw = await pubClient.hgetall(sessionKey);
|
|
if (!raw || Object.keys(raw).length === 0) return null;
|
|
|
|
return Object.entries(raw).reduce((acc, [field, rawValue]) => {
|
|
try {
|
|
acc[field] = JSON.parse(rawValue);
|
|
} catch {
|
|
acc[field] = rawValue;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
const data = await pubClient.hget(sessionKey, key);
|
|
return data ? JSON.parse(data) : null;
|
|
} catch (error) {
|
|
logger.log(`Error Getting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Store session transaction data in Redis
|
|
* @param socketId
|
|
* @param transactionType
|
|
* @param key
|
|
* @param value
|
|
* @param ttl
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
|
|
try {
|
|
await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, toRedisJson(value)); // Use Redis pubClient
|
|
if (ttl && typeof ttl === "number") {
|
|
await pubClient.expire(getSocketTransactionkey({ socketId, transactionType }), ttl);
|
|
}
|
|
} catch (error) {
|
|
logger.log(
|
|
`Error Setting Session Data for socket transaction ${socketId}:${transactionType}: ${error}`,
|
|
"ERROR",
|
|
"redis"
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Retrieve session transaction data from Redis
|
|
* @param socketId
|
|
* @param transactionType
|
|
* @param key
|
|
* @returns {Promise<any|null>}
|
|
*/
|
|
const getSessionTransactionData = async (socketId, transactionType, key) => {
|
|
try {
|
|
const data = await pubClient.hget(getSocketTransactionkey({ socketId, transactionType }), key);
|
|
return data ? JSON.parse(data) : null;
|
|
} catch (error) {
|
|
logger.log(
|
|
`Error Getting Session Data for socket transaction ${socketId}:${transactionType}: ${error}`,
|
|
"ERROR",
|
|
"redis"
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clear session data from Redis
|
|
* @param socketId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const clearSessionData = async (socketId) => {
|
|
try {
|
|
await pubClient.del(`socket:${socketId}`);
|
|
} catch (error) {
|
|
logger.log(`Error Clearing Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clear session transaction data from Redis
|
|
* @param socketId
|
|
* @param transactionType
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const clearSessionTransactionData = async (socketId, transactionType) => {
|
|
try {
|
|
if (transactionType) {
|
|
await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
|
|
return;
|
|
}
|
|
|
|
// If no transactionType is provided, clear all transaction namespaces for this socket.
|
|
const pattern = getSocketTransactionkey({ socketId, transactionType: "*" });
|
|
const keys = await pubClient.keys(pattern);
|
|
if (Array.isArray(keys) && keys.length > 0) {
|
|
await pubClient.del(...keys);
|
|
}
|
|
} catch (error) {
|
|
logger.log(
|
|
`Error Clearing Session Transaction Data for socket ${socketId}:${transactionType}: ${error}`,
|
|
"ERROR",
|
|
"redis"
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add a socket mapping for a user
|
|
* @param email
|
|
* @param socketId
|
|
* @param bodyshopId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const addUserSocketMapping = async (email, socketId, bodyshopId) => {
|
|
const socketMappingKey = getUserSocketMappingKey(email);
|
|
try {
|
|
devDebugLogger(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`);
|
|
// Save the mapping: socketId -> bodyshopId
|
|
await pubClient.hset(socketMappingKey, socketId, bodyshopId);
|
|
// Set TTL (24 hours) for the mapping hash
|
|
await pubClient.expire(socketMappingKey, 86400);
|
|
} catch (error) {
|
|
logger.log(`Error adding socket mapping for ${email} (bodyshop ${bodyshopId}): ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Refresh the TTL for a user's socket mapping
|
|
* @param email
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const refreshUserSocketTTL = async (email) => {
|
|
const socketMappingKey = getUserSocketMappingKey(email);
|
|
|
|
try {
|
|
const exists = await pubClient.exists(socketMappingKey);
|
|
if (exists) {
|
|
await pubClient.expire(socketMappingKey, 86400);
|
|
devDebugLogger(`Refreshed TTL for ${email} socket mapping`);
|
|
}
|
|
} catch (error) {
|
|
logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove a socket mapping for a user
|
|
* @param email
|
|
* @param socketId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const removeUserSocketMapping = async (email, socketId) => {
|
|
const socketMappingKey = getUserSocketMappingKey(email);
|
|
|
|
try {
|
|
devDebugLogger(`Removing socket ${socketId} mapping for user ${email}`);
|
|
// Look up the bodyshopId associated with this socket
|
|
const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
|
|
if (!bodyshopId) {
|
|
devDebugLogger(`Socket ${socketId} not found for user ${email}`);
|
|
return;
|
|
}
|
|
// Remove the socket mapping
|
|
await pubClient.hdel(socketMappingKey, socketId);
|
|
devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`);
|
|
|
|
// Refresh TTL if any socket mappings remain
|
|
const remainingSockets = await pubClient.hlen(socketMappingKey);
|
|
if (remainingSockets > 0) {
|
|
await pubClient.expire(socketMappingKey, 86400);
|
|
}
|
|
} catch (error) {
|
|
logger.log(`Error removing socket mapping for ${email}: ${error}`, "ERROR", "redis");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get all socket mappings for a user
|
|
* @param email
|
|
* @returns {Promise<{}>}
|
|
*/
|
|
const getUserSocketMapping = async (email) => {
|
|
const socketMappingKey = getUserSocketMappingKey(email);
|
|
|
|
try {
|
|
// Retrieve all socket mappings for the user
|
|
const mapping = await pubClient.hgetall(socketMappingKey);
|
|
const ttl = await pubClient.ttl(socketMappingKey);
|
|
// Group socket IDs by bodyshopId
|
|
const result = {};
|
|
for (const [socketId, bodyshopId] of Object.entries(mapping)) {
|
|
if (!result[bodyshopId]) {
|
|
result[bodyshopId] = { socketIds: [], ttl };
|
|
}
|
|
result[bodyshopId].socketIds.push(socketId);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error(`Error retrieving socket mappings for ${email}:`, error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get socket IDs for a user by bodyshopId
|
|
* @param email
|
|
* @param bodyshopId
|
|
* @returns {Promise<{socketIds: [string, string], ttl: *}>}
|
|
*/
|
|
const getUserSocketMappingByBodyshop = async (email, bodyshopId) => {
|
|
const socketMappingKey = getUserSocketMappingKey(email);
|
|
|
|
try {
|
|
// Retrieve all socket mappings for the user
|
|
const mapping = await pubClient.hgetall(socketMappingKey);
|
|
const ttl = await pubClient.ttl(socketMappingKey);
|
|
// Filter socket IDs for the provided bodyshopId
|
|
const socketIds = Object.entries(mapping).reduce((acc, [socketId, bId]) => {
|
|
if (bId === bodyshopId) {
|
|
acc.push(socketId);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
return { socketIds, ttl };
|
|
} catch (error) {
|
|
logger.log(`Error retrieving socket mappings for ${email} by bodyshop ${bodyshopId}: ${error}`, "ERROR", "redis");
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get bodyshop data from Redis
|
|
* @param bodyshopId
|
|
* @returns {Promise<*>}
|
|
*/
|
|
const getBodyshopFromRedis = async (bodyshopId) => {
|
|
const key = getBodyshopCacheKey(bodyshopId);
|
|
try {
|
|
// Check if data exists in Redis
|
|
const cachedData = await pubClient.get(key);
|
|
if (cachedData) {
|
|
return JSON.parse(cachedData); // Parse and return the full object
|
|
}
|
|
|
|
// Cache miss: fetch from DB
|
|
const bodyshopData = await fetchBodyshopFromDB(bodyshopId, logger);
|
|
|
|
// Store in Redis as a single JSON string
|
|
const jsonData = JSON.stringify(bodyshopData);
|
|
await pubClient.set(key, jsonData);
|
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
|
|
|
devDebugLogger("bodyshop-cache-miss", {
|
|
bodyshopId,
|
|
action: "Fetched from DB and cached"
|
|
});
|
|
|
|
return bodyshopData; // Return the full object
|
|
} catch (error) {
|
|
logger.log("get-bodyshop-from-redis", "ERROR", "redis", null, {
|
|
bodyshopId,
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update or invalidate bodyshop data in Redis
|
|
* @param bodyshopId
|
|
* @param values
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
|
|
const key = getBodyshopCacheKey(bodyshopId);
|
|
try {
|
|
if (!values) {
|
|
// Invalidate cache by deleting the key
|
|
await pubClient.del(key);
|
|
devDebugLogger("bodyshop-cache-invalidate", {
|
|
bodyshopId,
|
|
action: "Cache invalidated"
|
|
});
|
|
} else {
|
|
// Update cache with the full provided values
|
|
const jsonData = JSON.stringify(values);
|
|
await pubClient.set(key, jsonData);
|
|
await pubClient.expire(key, BODYSHOP_CACHE_TTL);
|
|
devDebugLogger("bodyshop-cache-update", {
|
|
bodyshopId,
|
|
action: "Cache updated",
|
|
values
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.log("update-or-invalidate-bodyshop-from-redis", "ERROR", "api", "redis", {
|
|
bodyshopId,
|
|
values,
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set provider cache data
|
|
* @param ns
|
|
* @param field
|
|
* @param value
|
|
* @param ttl
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl);
|
|
|
|
/**
|
|
* Get provider cache data
|
|
* @param ns
|
|
* @param field
|
|
* @returns {Promise<any|null|undefined>}
|
|
*/
|
|
const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field);
|
|
|
|
/**
|
|
* Get Chatter API token from Redis cache
|
|
* @param companyId
|
|
* @returns {Promise<string|null>}
|
|
*/
|
|
const getChatterToken = async (companyId) => {
|
|
const key = getChatterTokenCacheKey(companyId);
|
|
try {
|
|
const token = await pubClient.get(key);
|
|
return token;
|
|
} catch (error) {
|
|
logger.log("get-chatter-token-from-redis", "ERROR", "redis", null, {
|
|
companyId,
|
|
error: error.message
|
|
});
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set Chatter API token in Redis cache
|
|
* @param companyId
|
|
* @param token
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const setChatterToken = async (companyId, token) => {
|
|
const key = getChatterTokenCacheKey(companyId);
|
|
try {
|
|
await pubClient.set(key, token);
|
|
await pubClient.expire(key, CHATTER_TOKEN_CACHE_TTL);
|
|
devDebugLogger("chatter-token-cache-set", {
|
|
companyId,
|
|
action: "Token cached"
|
|
});
|
|
} catch (error) {
|
|
logger.log("set-chatter-token-in-redis", "ERROR", "redis", null, {
|
|
companyId,
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const api = {
|
|
getUserSocketMappingKey,
|
|
getBodyshopCacheKey,
|
|
getChatterTokenCacheKey,
|
|
setSessionData,
|
|
getSessionData,
|
|
clearSessionData,
|
|
addUserSocketMapping,
|
|
removeUserSocketMapping,
|
|
getUserSocketMappingByBodyshop,
|
|
getUserSocketMapping,
|
|
refreshUserSocketTTL,
|
|
getBodyshopFromRedis,
|
|
updateOrInvalidateBodyshopFromRedis,
|
|
setSessionTransactionData,
|
|
getSessionTransactionData,
|
|
clearSessionTransactionData,
|
|
setProviderCache,
|
|
getProviderCache,
|
|
getChatterToken,
|
|
setChatterToken
|
|
};
|
|
|
|
Object.assign(module.exports, api);
|
|
|
|
app.use((req, res, next) => {
|
|
req.sessionUtils = api;
|
|
next();
|
|
});
|
|
|
|
return api;
|
|
};
|
|
|
|
module.exports = { applyRedisHelpers };
|