@databuddy/cache
v0.0.120
Published
A generic caching implementation for Drizzle ORM that provides automatic query result caching and intelligent cache invalidation
Maintainers
Readme
@databuddy/cache
A Redis-based caching implementation for Drizzle ORM that provides automatic query result caching and intelligent cache invalidation.
Overview
@databuddy/cache extends Drizzle ORM's base Cache class to provide Redis-backed caching for database queries. It automatically caches query results and invalidates cache entries when mutations occur on tracked tables, ensuring data consistency while improving query performance.
Features
- Automatic query result caching with configurable TTL
- Intelligent cache invalidation based on table mutations
- Configurable caching strategies (explicit or automatic)
- Namespace support for cache key isolation
- TypeScript support with full type definitions
Installation
bun add @databuddy/cache drizzle-ormQuick Start
Basic Usage with ioredis
import Redis from "ioredis";
import { drizzle } from "drizzle-orm/node-postgres";
import { RedisDrizzleCache } from "@databuddy/cache";
import * as schema from "./schema";
// Create Redis client
const redis = new Redis(process.env.REDIS_URL!);
// Create cache instance
const cache = new RedisDrizzleCache({
redis,
defaultTtl: 300, // 5 minutes
strategy: "all", // Cache all queries
namespace: "myapp:drizzle"
});
// Use with Drizzle
const db = drizzle(connectionString, {
schema,
cache
});
// Queries are automatically cached
const users = await db.select().from(usersTable);
// Mutations automatically invalidate related cache entries
await db.update(usersTable).set({ name: "John" }).where(eq(usersTable.id, 1));
// Cache entries for 'users' table are now invalidatedConfiguration
RedisCacheConfig
type RedisCacheConfig = {
redis: RedisAdapter | unknown;
defaultTtl?: number;
strategy?: "explicit" | "all";
namespace?: string;
};redis
The Redis client instance from ioredis. Must be a connected Redis instance.
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);The client must support the following methods:
get(key: string): Promise<string | null>- Retrieve a value from Redissetex(key: string, seconds: number, value: string): Promise<unknown>- Set a value with TTLunlink(...keys: string[]): Promise<unknown>- Delete one or more keys (falls back todelif unavailable)
defaultTtl
Default time-to-live for cached entries in seconds. Used when no explicit TTL is provided via CacheConfig.
Default: 300 (5 minutes)
const cache = new RedisDrizzleCache({
redis,
defaultTtl: 600 // 10 minutes
});strategy
Cache strategy determines when queries are cached:
"all"- All queries are automatically cached (default)"explicit"- Only queries explicitly marked with.$withCache()are cached
Default: "all"
// Explicit strategy
const cache = new RedisDrizzleCache({
redis,
strategy: "explicit"
});
// Only explicitly marked queries are cached
const users = await db
.select()
.from(usersTable)
.$withCache({
key: "all-users",
ttl: 600,
tables: ["users"]
});namespace
Optional namespace prefix for all cache keys. Useful for isolating cache entries from different applications or environments.
Default: "drizzle"
const cache = new RedisDrizzleCache({
redis,
namespace: "myapp:drizzle" // Keys will be prefixed with "myapp:drizzle:..."
});Cache Configuration (CacheConfig)
When using explicit caching with .$withCache(), you can provide additional cache configuration:
await db.select().from(usersTable).$withCache({
key: "users",
ttl: 300,
tables: ["users"],
config: {
ex: 60, // Expire in 60 seconds
// OR
px: 60000, // Expire in 60000 milliseconds
// OR
exat: 1735689600, // Expire at Unix timestamp (seconds)
// OR
pxat: 1735689600000 // Expire at Unix timestamp (milliseconds)
}
});CacheConfig Options
ex- Expiration time in secondspx- Expiration time in millisecondsexat- Unix timestamp (seconds) at which the key will expirepxat- Unix timestamp (milliseconds) at which the key will expirekeepTtl- Retain existing TTL when updating a key
How It Works
Caching Strategy: "all"
When using the "all" strategy, all queries are automatically cached:
- Before executing a query, Drizzle checks the cache using the query hash as the key
- If found, the cached result is returned immediately
- If not found, the query executes and the result is stored in cache with the configured TTL
- The cache tracks which tables each cached query depends on
Cache Invalidation
When mutations (INSERT, UPDATE, DELETE) occur:
- Drizzle calls
onMutate()with the affected tables - The cache finds all cached queries associated with those tables
- Those cache entries are deleted
- Subsequent queries for those tables will execute fresh queries
Caching Strategy: "explicit"
When using the "explicit" strategy:
- Only queries explicitly marked with
.$withCache()are cached - You must provide a cache key, TTL, and associated tables
- Cache invalidation works the same way based on table mutations
API Reference
RedisDrizzleCache
Constructor
new RedisDrizzleCache(config: RedisCacheConfig): RedisDrizzleCacheCreates a new RedisDrizzleCache instance.
Methods
strategy()
strategy(): "explicit" | "all"Returns the cache strategy being used.
get()
get(key: string): Promise<any[] | undefined>Retrieves cached data for a given query key. Called automatically by Drizzle.
Parameters:
key- The hashed query key to look up in cache
Returns: The cached query result as an array, or undefined if not found
put()
put(
key: string,
response: any,
tables: string[],
isTag: boolean,
config?: CacheConfig
): Promise<void>Stores query results in the cache. Called automatically by Drizzle after executing a query.
Parameters:
key- The hashed query key used as the cache keyresponse- The query result to cache (will be JSON stringified)tables- Array of table names involved in the query (used for invalidation)isTag- Whether this is a tag-based cache entryconfig- Optional cache configuration for TTL and expiration
onMutate()
onMutate(params: {
tags?: string | string[];
tables?: string | string[] | Table<any> | Table<any>[];
}): Promise<void>Invalidates cache entries when mutations occur. Called automatically by Drizzle when mutations are executed.
Parameters:
params.tags- Optional tag(s) to invalidate (for tag-based invalidation)params.tables- Table(s) that were mutated (can be Table objects or strings)
Examples
Basic Setup
import Redis from "ioredis";
import { drizzle } from "drizzle-orm/node-postgres";
import { RedisDrizzleCache } from "@databuddy/cache";
import { users, posts } from "./schema";
const redis = new Redis(process.env.REDIS_URL!);
const cache = new RedisDrizzleCache({
redis,
defaultTtl: 300,
strategy: "all",
namespace: "myapp"
});
const db = drizzle(connectionString, {
schema: { users, posts },
cache
});Explicit Caching
const cache = new RedisDrizzleCache({
redis,
strategy: "explicit"
});
// Only this query will be cached
const popularPosts = await db
.select()
.from(posts)
.where(gt(posts.views, 1000))
.$withCache({
key: "popular-posts",
ttl: 600,
tables: ["posts"]
});Custom TTL per Query
// Using explicit caching with custom TTL
const users = await db
.select()
.from(usersTable)
.$withCache({
key: "all-users",
ttl: 1800, // 30 minutes
tables: ["users"],
config: {
ex: 1800
}
});Error Handling
The cache implementation handles errors gracefully:
- Cache GET failures return
undefinedand log errors to console - Cache PUT failures log errors but don't throw
- Cache invalidation failures are logged but don't interrupt the mutation
This ensures that cache failures don't break your application - queries will simply execute without caching.
Performance Considerations
- Cache hits avoid database queries entirely, significantly improving response times
- Cache invalidation is performed in parallel for better performance
- Table-to-key tracking is maintained in memory for fast invalidation lookups
- Redis operations are non-blocking and asynchronous
TypeScript Support
Full TypeScript support is provided with comprehensive type definitions:
import type { RedisCacheConfig } from "@databuddy/cache";
const config: RedisCacheConfig = {
redis: myRedisClient,
defaultTtl: 300,
strategy: "all",
namespace: "myapp"
};Requirements
- Drizzle ORM ^0.45.1
- ioredis ^5.8.2
- A Redis server (local or remote)
License
See the main repository license.
