UNPKG

@paulwer/prisma-extension-cache-manager

Version:

A caching extension for [Prisma](https://www.prisma.io/), fully compatible with [cache-manager](https://www.npmjs.com/package/cache-manager), predefined uncaching strategies and custom handlers for key generation and uncaching.

246 lines (245 loc) 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const types_1 = require("./types"); const methods_1 = require("./methods"); const client_1 = require("@prisma/client"); const crypto_1 = require("crypto"); const promiseCache = {}; exports.default = ({ cache, defaultTTL, useAutoUncache, useDeduplication, prisma, typePrefixes, }) => { if (prisma && !prisma.defineExtension) throw new Error('Prisma object is invalid. Please provide a valid Prisma object by using the following: import { Prisma } from "@prisma/client"'); async function safeDelete(keys) { for (const store of cache.stores) for (const key of keys) await store.delete(key); // Delete the key from each store } const Prisma = prisma || client_1.Prisma; return Prisma.defineExtension({ name: "prisma-extension-cache-manager", model: { $allModels: {}, }, client: { $cache: cache, async $queryRawCached(sql, cacheOption) { const context = Prisma.getExtensionContext(this); const processUncache = async (result) => { const option = cacheOption?.uncache; let keysToDelete = []; if (typeof option === "function") { const keys = option(result); keysToDelete = Array.isArray(keys) ? keys : [keys]; } else if (typeof option === "string") { keysToDelete = [option]; } else if (Array.isArray(option)) { if (typeof option[0] === "string") { keysToDelete = option; } else if (typeof option[0] === "object") { keysToDelete = option.map((obj) => obj.namespace ? `${obj.namespace}:${obj.key}` : obj.key); } } if (keysToDelete.length) await safeDelete(keysToDelete); }; const useUncache = cacheOption?.uncache !== undefined && (typeof cacheOption?.uncache === "function" || typeof cacheOption?.uncache === "string" || Array.isArray(cacheOption?.uncache)); const cacheTTL = typeof cacheOption?.cache === "number" ? cacheOption?.cache : typeof cacheOption?.cache === "object" ? (cacheOption?.cache?.ttl ?? defaultTTL) : defaultTTL; const cacheKey = (0, methods_1.generateComposedKey)({ model: "$queryRaw", operation: (0, crypto_1.createHash)("md5") .update(JSON.stringify(sql.strings)) .digest("hex"), queryArgs: sql.values, }); const cached = await cache.get(cacheKey); if (cached) return (0, methods_1.deserializeData)(cached, typePrefixes); let queryPromise; if (useDeduplication) { if (!promiseCache[cacheKey]) promiseCache[cacheKey] = context.$queryRaw(sql); queryPromise = promiseCache[cacheKey]; } else queryPromise = context.$queryRaw(sql); const result = await queryPromise.finally(() => delete promiseCache[cacheKey]); if (useUncache) await processUncache(result); await cache.set(cacheKey, (0, methods_1.serializeData)(result, typePrefixes), cacheTTL); if (useUncache) await processUncache(result); return result; }, async $queryRawUnsafeCached(sql, cacheOption) { const context = Prisma.getExtensionContext(this); const processUncache = async (result) => { const option = cacheOption?.uncache; let keysToDelete = []; if (typeof option === "function") { const keys = option(result); keysToDelete = Array.isArray(keys) ? keys : [keys]; } else if (typeof option === "string") { keysToDelete = [option]; } else if (Array.isArray(option)) { if (typeof option[0] === "string") { keysToDelete = option; } else if (typeof option[0] === "object") { keysToDelete = option.map((obj) => obj.namespace ? `${obj.namespace}:${obj.key}` : obj.key); } } if (keysToDelete.length) await safeDelete(keysToDelete); }; const useUncache = cacheOption?.uncache !== undefined && (typeof cacheOption?.uncache === "function" || typeof cacheOption?.uncache === "string" || Array.isArray(cacheOption?.uncache)); const cacheTTL = typeof cacheOption?.cache === "number" ? cacheOption?.cache : typeof cacheOption?.cache === "object" ? (cacheOption?.cache?.ttl ?? defaultTTL) : defaultTTL; const cacheKey = (0, methods_1.generateComposedKey)({ model: "$queryRawUnsafe", operation: (0, crypto_1.createHash)("md5").update(sql).digest("hex"), queryArgs: {}, }); const cached = await cache.get(cacheKey); if (cached) return (0, methods_1.deserializeData)(cached, typePrefixes); let queryPromise; if (useDeduplication) { if (!promiseCache[cacheKey]) promiseCache[cacheKey] = context.$queryRawUnsafe(sql); queryPromise = promiseCache[cacheKey]; } else queryPromise = context.$queryRawUnsafe(sql); const result = await queryPromise.finally(() => delete promiseCache[cacheKey]); if (useUncache) await processUncache(result); await cache.set(cacheKey, (0, methods_1.serializeData)(result, typePrefixes), cacheTTL); if (useUncache) await processUncache(result); return result; }, }, query: { $allModels: { async $allOperations({ model, operation, args, query }) { if (!types_1.CACHE_OPERATIONS.includes(operation)) return query(args); const isWriteOperation = types_1.WRITE_OPERATIONS.includes(operation); const { cache: cacheOption, uncache: uncacheOption, ...queryArgs } = args; const processUncache = async (result) => { const option = uncacheOption; let keysToDelete = []; if (typeof option === "function") { const keys = option(result); keysToDelete = Array.isArray(keys) ? keys : [keys]; } else if (typeof option === "string") { keysToDelete = [option]; } else if (Array.isArray(option)) { if (typeof option[0] === "string") { keysToDelete = option; } else if (typeof option[0] === "object") { keysToDelete = option.map((obj) => obj.namespace ? `${obj.namespace}:${obj.key}` : obj.key); } } if (keysToDelete.length) await safeDelete(keysToDelete); }; const processAutoUncache = async () => { const keysToDelete = []; const models = (0, methods_1.getInvolvedModels)(Prisma, model, operation, args); await Promise.all(models.map((model) => (async () => { for (const store of cache.stores) if (store?.iterator) { for await (const [key] of store.iterator({})) { if (key.includes(`:${model}:`)) keysToDelete.push(key); } } })())); await safeDelete(keysToDelete); }; const useCache = cacheOption !== undefined && ["boolean", "object", "number", "string"].includes(typeof cacheOption) && !(typeof cacheOption === "boolean" && cacheOption === false); const useUncache = uncacheOption !== undefined && (typeof uncacheOption === "function" || typeof uncacheOption === "string" || Array.isArray(uncacheOption)); const cacheTTL = typeof cacheOption === "number" ? cacheOption : typeof cacheOption === "object" ? (cacheOption.ttl ?? defaultTTL) : defaultTTL; if (!useCache) { const result = await query(queryArgs); if (useUncache) await processUncache(result); if (useAutoUncache && isWriteOperation) await processAutoUncache(); return result; } if (typeof cacheOption.key === "function") { const result = await query(queryArgs); if (useUncache) await processUncache(result); if (useAutoUncache && isWriteOperation) await processAutoUncache(); const customCacheKey = cacheOption.key(result); cache.set(customCacheKey, (0, methods_1.serializeData)(result, typePrefixes), cacheTTL); return result; } const cacheKey = typeof cacheOption === "string" ? cacheOption : cacheOption.key ? (0, methods_1.createKey)(cacheOption.key, cacheOption.namespace) : (0, methods_1.generateComposedKey)({ model, operation, namespace: cacheOption.namespace, queryArgs, }); if (!isWriteOperation) { const cached = await cache.get(cacheKey); if (cached) return (0, methods_1.deserializeData)(cached, typePrefixes); } let queryPromise; if (useDeduplication) { if (!promiseCache[cacheKey]) promiseCache[cacheKey] = query(queryArgs); queryPromise = promiseCache[cacheKey]; } else queryPromise = query(queryArgs); const result = await queryPromise.finally(() => delete promiseCache[cacheKey]); if (useUncache) await processUncache(result); if (useAutoUncache && isWriteOperation) await processAutoUncache(); await cache.set(cacheKey, (0, methods_1.serializeData)(result, typePrefixes), cacheTTL); return result; }, }, }, }); };