UNPKG

@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
"use strict"; 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;