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

195 lines (194 loc) 8.52 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.createQuellRouter = void 0; const crypto_1 = require("crypto"); /** * Creates a router middleware for handling GraphQL requests to multiple endpoints * * @param options Configuration options * @returns Express middleware function */ function createQuellRouter(options) { const { endpoints, cache, cacheExpiration = 3600, debug = false, headers = {}, } = options; // Create router middleware function const routerMiddleware = (req, res, next) => __awaiter(this, void 0, void 0, function* () { try { // Skip non-POST requests if (req.method !== "POST") { return next(); } // Get the path and check if it's a configured endpoint const path = req.path; const targetUrl = endpoints[path]; // If path not found or marked as 'local', pass to next middleware if (!targetUrl || targetUrl === "local") { if (debug) console.log(`QUELL ROUTER: Path ${path} not found or marked as local, passing to next middleware`); return next(); } // Get the API name from the path (for cache namespacing) const apiName = path.split("/").pop() || "api"; // Extract GraphQL query, variables, and operation name const { query, variables, operationName } = req.body; // Skip if no query provided if (!query) { if (debug) console.log("QUELL ROUTER: No query provided, passing to next middleware"); return next(); } if (debug) { console.log(`QUELL ROUTER: Processing ${apiName} query`); console.log("QUERY:", query); console.log("VARIABLES:", variables); } // Generate a cache key based on the API, query, and variables const queryHash = generateHash({ query, variables, operationName }); const cacheKey = `quellrouter:${apiName}:${queryHash}`; try { // Check if response is cached const cachedResponse = yield cache.get(cacheKey); if (cachedResponse) { if (debug) console.log(`QUELL ROUTER: Cache hit for ${apiName}`); // Parse the cached response const parsedResponse = JSON.parse(cachedResponse); // Add cacheHit flag for cache hits parsedResponse.cacheHit = true; // Send the cached response return res.json(parsedResponse); } } catch (cacheError) { console.error(`QUELL ROUTER: Cache read error:`, cacheError); // Continue even if cache read fails } if (debug) console.log(`QUELL ROUTER: Cache miss for ${apiName}, fetching from API at ${targetUrl}`); // Get API-specific headers const apiHeaders = headers[apiName] || {}; // Execute the query against the target API using native fetch const response = yield fetch(targetUrl, { method: "POST", headers: Object.assign(Object.assign({ "Content-Type": "application/json", Accept: "application/json" }, apiHeaders), (req.headers.authorization ? { Authorization: req.headers.authorization } : {})), body: JSON.stringify({ query, variables, operationName, }), }); // Check for HTTP errors if (!response.ok) { const errorText = yield response.text(); throw new Error(`GraphQL API error (${response.status}): ${errorText}`); } // Parse the API response const apiResponse = yield response.json(); // Add cacheHit flag for fresh API responses apiResponse.cacheHit = false; if (debug) { console.log(`QUELL ROUTER: Received response from ${apiName} API`); if (apiResponse.errors) { console.log("RESPONSE ERRORS:", apiResponse.errors); } } // Cache the response if there are no errors if (!apiResponse.errors) { try { // Remove cacheHit flag before caching (optional - keeps cache clean) const responseToCache = Object.assign({}, apiResponse); delete responseToCache.cacheHit; // Stringify the response before caching const stringifiedResponse = JSON.stringify(responseToCache); // Use SET with EX option to set expiration yield cache.set(cacheKey, stringifiedResponse, { EX: cacheExpiration, }); if (debug) console.log(`QUELL ROUTER: Cached response for ${apiName} (TTL: ${cacheExpiration}s)`); } catch (cacheError) { console.error(`QUELL ROUTER: Cache write error:`, cacheError); // Continue even if cache write fails } } // Send the API response return res.json(apiResponse); } catch (error) { console.error("QUELL ROUTER ERROR:", error); // Return a GraphQL-formatted error response return res.status(500).json({ errors: [ { message: `Error in GraphQL router: ${error instanceof Error ? error.message : String(error)}`, extensions: { code: "ROUTER_ERROR" }, }, ], }); } }); // Add utility methods to the middleware function /** * Clears cache for a specific API */ routerMiddleware.clearApiCache = (apiName) => __awaiter(this, void 0, void 0, function* () { try { const pattern = `quellrouter:${apiName}:*`; const keys = yield cache.keys(pattern); if (keys.length > 0) { yield cache.del(keys); if (debug) console.log(`QUELL ROUTER: Cleared ${keys.length} cached responses for ${apiName}`); return keys.length; } return 0; } catch (error) { console.error(`QUELL ROUTER: Error clearing API cache:`, error); return 0; } }); /** * Clears all router cache entries */ routerMiddleware.clearAllCache = () => __awaiter(this, void 0, void 0, function* () { try { const pattern = "quellrouter:*"; const keys = yield cache.keys(pattern); if (keys.length > 0) { yield cache.del(keys); if (debug) console.log(`QUELL ROUTER: Cleared ${keys.length} cached responses`); return keys.length; } return 0; } catch (error) { console.error(`QUELL ROUTER: Error clearing all cache:`, error); return 0; } }); return routerMiddleware; } exports.createQuellRouter = createQuellRouter; /** * Generates a deterministic hash for cache keys */ function generateHash(data) { const stringified = JSON.stringify(data); return (0, crypto_1.createHash)("sha256") .update(stringified) .digest("hex") .substring(0, 16); }