@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
204 lines (203 loc) • 8.79 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.extractCacheKeys = exports.shouldCacheResponse = exports.createHandleQueryCaching = exports.createBuildCacheFromMergedResponse = exports.createBuildCacheFromResponse = void 0;
/**
* Creates a function to build cache from GraphQL query response
* This function takes the response from a GraphQL query and stores it in cache
* @param config - Configuration object
* @returns Bound buildCacheFromResponse function
*/
function createBuildCacheFromResponse(config) {
const { queryMap, writeToCache, normalizeForCache } = config;
/**
* Builds cache entries from a GraphQL query response
* @param queryResponse - The response from GraphQL query execution
* @param prototype - The prototype object describing the query structure
* @param operationType - The type of operation (query, mutation, etc.)
*/
return function buildCacheFromResponse(queryResponse, prototype, operationType) {
return __awaiter(this, void 0, void 0, function* () {
try {
// Skip caching for mutations as they're handled separately
if (operationType === 'mutation') {
return;
}
// Extract data from the response
const responseData = queryResponse.data;
if (!responseData) {
console.warn('No data in query response to cache');
return;
}
// Normalize and cache the response data
yield normalizeForCache(responseData, queryMap, prototype, 'root' // Default parent name for root queries
);
console.log('Successfully built cache from query response');
}
catch (error) {
const err = {
log: `Error building cache from response: ${error}`,
status: 500,
message: {
err: 'Error building cache from response. Check server log for details.',
},
};
console.error(err);
throw error;
}
});
};
}
exports.createBuildCacheFromResponse = createBuildCacheFromResponse;
/**
* Creates a function to build cache from merged response
* This handles the case where responses are merged from cache and database
* @param config - Configuration object
* @returns Bound buildCacheFromMergedResponse function
*/
function createBuildCacheFromMergedResponse(config) {
const { queryMap, normalizeForCache } = config;
/**
* Builds cache entries from a merged response (cache + database)
* @param mergedResponse - The merged response containing both cached and fresh data
* @param prototype - The prototype object describing the query structure
* @param queryMap - Map of queries to their types
*/
return function buildCacheFromMergedResponse(mergedResponse, prototype, queryMap) {
return __awaiter(this, void 0, void 0, function* () {
try {
// Extract data from the merged response
const responseData = mergedResponse.data;
if (!responseData) {
console.warn('No data in merged response to cache');
return;
}
// Normalize and cache the merged response data
yield normalizeForCache(responseData, queryMap, prototype, 'root' // Default parent name for root queries
);
console.log('Successfully built cache from merged response');
}
catch (error) {
const err = {
log: `Error building cache from merged response: ${error}`,
status: 500,
message: {
err: 'Error building cache from merged response. Check server log for details.',
},
};
console.error(err);
throw error;
}
});
};
}
exports.createBuildCacheFromMergedResponse = createBuildCacheFromMergedResponse;
/**
* Creates a function to handle caching logic in the query method
* This is the main integration point for caching query responses
* @param config - Configuration object
* @returns Function that handles the caching logic
*/
function createHandleQueryCaching(config) {
const { buildCacheFromResponse, buildCacheFromMergedResponse, queryMap } = config;
/**
* Handles caching for different query scenarios
* @param scenario - The caching scenario (full, partial, none)
* @param response - The response to cache
* @param prototype - The query prototype
* @param operationType - The operation type
*/
return function handleQueryCaching(scenario, response, prototype, operationType) {
return __awaiter(this, void 0, void 0, function* () {
try {
switch (scenario) {
case 'full':
// Full response from database, cache everything
yield buildCacheFromResponse(response, prototype, operationType);
break;
case 'partial':
// Merged response from cache and database
yield buildCacheFromMergedResponse(response, prototype, queryMap);
break;
case 'none':
// Everything was in cache, no need to cache
console.log('Response fully served from cache, no caching needed');
break;
default:
console.warn(`Unknown caching scenario: ${scenario}`);
}
}
catch (error) {
console.error('Error in handleQueryCaching:', error);
// Don't throw - caching errors shouldn't break the response
}
});
};
}
exports.createHandleQueryCaching = createHandleQueryCaching;
/**
* Helper function to determine if a response should be cached
* @param operationType - The type of GraphQL operation
* @param responseData - The response data
* @returns Whether the response should be cached
*/
function shouldCacheResponse(operationType, responseData) {
// Don't cache mutations (handled separately)
if (operationType === 'mutation') {
return false;
}
// Don't cache if no data
if (!responseData || !responseData.data) {
return false;
}
// Don't cache error responses
if (responseData.errors && responseData.errors.length > 0) {
return false;
}
// Don't cache unquellable operations
if (operationType === 'unQuellable') {
return false;
}
// Don't cache operations without ID
if (operationType === 'noID') {
return false;
}
return true;
}
exports.shouldCacheResponse = shouldCacheResponse;
/**
* Helper function to extract cache keys from a prototype
* @param prototype - The query prototype
* @returns Array of cache keys that should be built
*/
function extractCacheKeys(prototype) {
const cacheKeys = [];
function traverse(obj, parentKey = '') {
for (const key in obj) {
if (key.startsWith('__'))
continue; // Skip meta fields
const value = obj[key];
const fullKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof value === 'object' && value !== null) {
// If it has __type and __id, it's a cacheable entity
if (value.__type && value.__id) {
const cacheKey = `${value.__type}--${value.__id}`;
cacheKeys.push(cacheKey);
}
// Recursively traverse nested objects
traverse(value, fullKey);
}
}
}
traverse(prototype);
return cacheKeys;
}
exports.extractCacheKeys = extractCacheKeys;