@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
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.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);
}