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