@quell/server
Version:
Quell is an open-source NPM package providing a light-weight caching layer implementation and cache invalidation for GraphQL responses on both the client- and server-side. Use Quell to prevent redundant client-side API requests and to minimize costly serv
301 lines (300 loc) • 15.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGenerateCacheID = exports.createBuildFromCache = void 0;
const redisHelpers_1 = require("../helpers/redisHelpers");
/**
* Creates a buildFromCache function with the provided configuration
* @param config - Configuration object containing Redis cache, batch size, and ID cache
* @returns Bound buildFromCache function
*/
function createBuildFromCache(config) {
const { redisCache, redisReadBatchSize, idCache, generateCacheID } = config;
/**
* Helper function to generate cache ID with fallback logic
*/
const getCacheIDWithFallback = (prototype, subID = false) => {
let cacheID;
if (typeof subID === "string") {
cacheID = subID;
}
else {
cacheID = generateCacheID(prototype);
}
console.log(`Generated base cache ID: ${cacheID}`);
// Get key name from args if available
let keyName;
if (prototype.__args === null) {
keyName = undefined;
}
else {
keyName = Object.values(prototype.__args)[0];
}
console.log(`Key name from args: ${keyName}`);
// Check ID cache for mapping
if (keyName && idCache[keyName]) {
console.log(`Found ID cache for ${keyName}:`, idCache[keyName]);
if (idCache[keyName][cacheID]) {
const mappedValue = idCache[keyName][cacheID];
// Handle both string and array cases
if (typeof mappedValue === "string") {
cacheID = mappedValue;
console.log(`Updated cache ID from ID cache: ${cacheID}`);
}
else if (Array.isArray(mappedValue) && mappedValue.length > 0) {
// If it's an array, use the first value (they should all be the same anyway)
cacheID = mappedValue[0];
console.log(`Updated cache ID from ID cache array: ${cacheID}`);
}
}
// Try capitalized version
const capitalized = cacheID.charAt(0).toUpperCase() + cacheID.slice(1);
if (idCache[keyName][capitalized]) {
const mappedValue = idCache[keyName][capitalized];
// Handle both string and array cases
if (typeof mappedValue === "string") {
cacheID = mappedValue;
console.log(`Updated cache ID from capitalized version: ${cacheID}`);
}
else if (Array.isArray(mappedValue) && mappedValue.length > 0) {
// If it's an array, use the first value
cacheID = mappedValue[0];
console.log(`Updated cache ID from capitalized array: ${cacheID}`);
}
}
}
console.log(`Final cache ID: ${cacheID}`);
return cacheID;
};
/**
* Process array items from cache
*/
const processArrayCache = (array, prototype, typeKey, prototypeKeys, itemFromCache) => __awaiter(this, void 0, void 0, function* () {
let redisRunQueue = redisCache.multi();
for (let i = 0; i < array.length; i++) {
if (typeof itemFromCache[typeKey] === "string") {
const getCommandCallback = (cacheResponse) => {
const tempObj = {};
if (cacheResponse) {
const interimCache = JSON.parse(cacheResponse);
for (const property in prototype[typeKey]) {
// If property exists, set on tempObj
if (Object.prototype.hasOwnProperty.call(interimCache, property) &&
!property.includes("__")) {
tempObj[property] = interimCache[property];
}
// If prototype is nested at this field, recurse
else if (!property.includes("__") &&
typeof prototype[typeKey][property] ===
"object") {
buildFromCache(prototype[typeKey][property], prototypeKeys, {}, false, `${currTypeKey}--${property}`).then((tempData) => (tempObj[property] = tempData.data));
}
// If cache does not have property, set to false on prototype so that it is sent to GraphQL
else if (!property.includes("__") &&
typeof prototype[typeKey][property] !==
"object") {
prototype[typeKey][property] = false;
}
}
itemFromCache[typeKey][i] = tempObj;
}
// If there is nothing in the cache for this key, toggle all fields to false
else {
for (const property in prototype[typeKey]) {
if (!property.includes("__") &&
typeof prototype[typeKey][property] !==
"object") {
prototype[typeKey][property] = false;
}
}
}
};
const currTypeKey = itemFromCache[typeKey][i];
// Execute batch when reaching batch size
if (i !== 0 && i % redisReadBatchSize === 0) {
try {
const cacheResponseRaw = yield redisRunQueue.exec();
cacheResponseRaw.forEach((cacheResponse) => getCommandCallback(JSON.stringify(cacheResponse)));
}
catch (error) {
const err = {
log: `Error inside batch execution of buildFromCache, ${error}`,
status: 400,
message: {
err: "Error in buildFromCache. Check server log for more details.",
},
};
console.log(err);
}
redisRunQueue = redisCache.multi();
}
// Add get command to queue
redisRunQueue.get(currTypeKey.toLowerCase());
// Execute remaining items in queue
if (i === array.length - 1) {
try {
const cacheResponseRaw = yield redisRunQueue.exec();
cacheResponseRaw.forEach((cacheResponse) => getCommandCallback(JSON.stringify(cacheResponse)));
}
catch (error) {
const err = {
log: `Error inside final batch execution of buildFromCache, ${error}`,
status: 400,
message: {
err: "Error in buildFromCache. Check server log for more details.",
},
};
console.log(err);
}
}
}
}
});
/**
* Helper function to recursively mark all primitive fields in a nested object as false
* This ensures they get included in the database query
*/
const markNestedFieldsAsFalse = (obj) => {
for (const key in obj) {
if (key.includes("__"))
continue; // Skip meta fields
if (typeof obj[key] === "object" && obj[key] !== null) {
// Recursively mark nested objects
markNestedFieldsAsFalse(obj[key]);
}
else {
// Mark primitive fields as false
obj[key] = false;
}
}
};
/**
* Process nested cache queries
*/
const processNestedCache = (prototype, typeKey, itemFromCache, prototypeKeys, firstRun) => __awaiter(this, void 0, void 0, function* () {
if (!firstRun) {
// Handle recursive calls (non-root level)
// If this field is not in the cache, set to false
if ((itemFromCache === null ||
!Object.prototype.hasOwnProperty.call(itemFromCache, typeKey)) &&
typeof prototype[typeKey] !== "object" &&
!typeKey.includes("__") &&
!itemFromCache[0]) {
prototype[typeKey] = false;
}
// If this field is a nested query, recurse
if (!(Object.keys(itemFromCache).length > 0) &&
typeof itemFromCache === "object" &&
!typeKey.includes("__") &&
typeof prototype[typeKey] === "object") {
const cacheID = generateCacheID(prototype);
const cacheResponse = yield (0, redisHelpers_1.getFromRedis)(cacheID, redisCache);
if (cacheResponse)
itemFromCache[typeKey] = JSON.parse(cacheResponse);
yield buildFromCache(prototype[typeKey], prototypeKeys, itemFromCache[typeKey], false);
}
}
else {
// Handle root level processing (firstRun = true)
for (const field in prototype[typeKey]) {
// Skip meta fields
if (field.includes("__"))
continue;
const fieldPrototype = prototype[typeKey][field];
const isFieldObject = typeof fieldPrototype === "object";
const fieldExistsInCache = itemFromCache[typeKey] &&
Object.prototype.hasOwnProperty.call(itemFromCache[typeKey], field);
const cacheDataExists = itemFromCache[typeKey];
if (isFieldObject) {
// Handle nested object/array fields (like albums)
if (fieldExistsInCache) {
// Field exists in cache, recurse to process nested data
yield buildFromCache(fieldPrototype, prototypeKeys, itemFromCache[typeKey][field] || {}, false);
}
else {
// Field doesn't exist in cache - mark it for database retrieval
console.log(`Field '${field}' not found in cache, will query from database`);
// For nested objects, we need to mark all their primitive fields as false
// so that createQueryObj includes them in the database query
markNestedFieldsAsFalse(fieldPrototype);
}
}
else {
// Handle primitive fields (like id, name)
if (!fieldExistsInCache) {
if (cacheDataExists) {
// Cache has entity data but missing this field
prototype[typeKey][field] = false;
}
else {
// No cache data at all - mark field for database retrieval
prototype[typeKey][field] = false;
}
}
// If field exists in cache, leave it as is (it will be used from cache)
}
}
}
});
/**
* Main buildFromCache function
* Finds any requested information in the cache and assembles it on the cacheResponse.
* Uses the prototype as a template for cacheResponse and marks any data not found in the cache
* on the prototype for future retrieval from database.
*/
function buildFromCache(prototype, prototypeKeys, itemFromCache = {}, firstRun = true, subID = false) {
return __awaiter(this, void 0, void 0, function* () {
console.log("=== BUILD FROM CACHE START ===");
console.log("Prototype:", JSON.stringify(prototype, null, 2));
console.log("Prototype Keys:", prototypeKeys);
for (const typeKey in prototype) {
// If the current key is a root query, check cache and set any results to itemFromCache
if (prototypeKeys.includes(typeKey)) {
const cacheID = getCacheIDWithFallback(prototype[typeKey], subID);
console.log(`Looking for cache with key: ${cacheID}`);
const cacheResponse = yield (0, redisHelpers_1.getFromRedis)(cacheID, redisCache);
console.log(`Cache response for ${cacheID}:`, cacheResponse);
itemFromCache[typeKey] = cacheResponse ? JSON.parse(cacheResponse) : {};
}
// If itemFromCache at the current key is an array, iterate through and gather data
if (Array.isArray(itemFromCache[typeKey])) {
const array = itemFromCache[typeKey];
yield processArrayCache(array, prototype, typeKey, prototypeKeys, itemFromCache);
}
// Process nested cache queries
else if (typeof prototype[typeKey] === "object" &&
!typeKey.includes("__")) {
yield processNestedCache(prototype, typeKey, itemFromCache, prototypeKeys, firstRun);
}
}
console.log("=== BUILD FROM CACHE END ===");
console.log("Final itemFromCache:", JSON.stringify(itemFromCache, null, 2));
// Return itemFromCache on a data property to resemble GraphQL response format
return { data: itemFromCache };
});
}
return buildFromCache;
}
exports.createBuildFromCache = createBuildFromCache;
/**
* Creates a generateCacheID function
* Helper function that creates cacheIDs based on information from the prototype
* in the format of 'field--ID'
*/
function createGenerateCacheID() {
return function generateCacheID(queryProto) {
const cacheID = queryProto.__id
? `${queryProto.__type}--${queryProto.__id}`
: queryProto.__type;
return cacheID;
};
}
exports.createGenerateCacheID = createGenerateCacheID;