@pgcache/nest
v0.1.0
Published
NestJS integration for pgcache
Readme
@pgcache/nest
NestJS integration for pgcache - A Redis-like cache using PostgreSQL.
Features
- Easy integration with NestJS dependency injection
- Support for both sync and async configuration
- Lifecycle management (automatic cleanup on module destroy)
- Full TypeScript support
- Compatible with ConfigService for dynamic configuration
Installation
pnpm add @pgcache/nest @pgcache/core pgQuick Start
Basic Usage (Synchronous)
import { Module } from "@nestjs/common";
import { PgCacheModule } from "@pgcache/nest";
@Module({
imports: [
PgCacheModule.forRoot({
connectionString: process.env.DATABASE_URL,
}),
],
})
export class AppModule {}Using in a Service
import { Injectable } from "@nestjs/common";
import { PgCacheService } from "@pgcache/nest";
@Injectable()
export class UserService {
constructor(private readonly cache: PgCacheService) {}
async getUser(id: string) {
// Try to get from cache
const cached = await this.cache.get<User>(`user:${id}`);
if (cached) return cached;
// Fetch from database
const user = await this.userRepository.findById(id);
// Store in cache with 5 minute TTL
await this.cache.set(`user:${id}`, user, { ttl: 300 });
return user;
}
async invalidateUser(id: string) {
await this.cache.del(`user:${id}`);
}
}Distributed Lock Example
Use setNX with delIfEquals for safe distributed locks:
import { Injectable } from "@nestjs/common";
import { randomUUID } from "crypto";
import { PgCacheService } from "@pgcache/nest";
@Injectable()
export class TaskService {
constructor(private readonly cache: PgCacheService) {}
async processTask(taskId: string) {
const lockKey = `lock:task:${taskId}`;
const lockToken = randomUUID(); // Unique token for ownership
// Try to acquire lock with 30 second TTL
const acquired = await this.cache.setNX(
lockKey,
lockToken,
{ ttl: 30 }
);
if (!acquired) {
console.log("Task already being processed");
return;
}
try {
// Do work...
await this.doWork(taskId);
} finally {
// Safely release only if we still own the lock
await this.cache.delIfEquals(lockKey, lockToken);
}
}
private async doWork(taskId: string) {
// Your task processing logic
}
}Important: Always use delIfEquals() to release locks, not del(). Using plain del() is unsafe because if your lock expires while processing, another process can acquire it, and your del() will delete their lock.
Advanced Configuration
Async Configuration with ConfigService
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { PgCacheModule } from "@pgcache/nest";
@Module({
imports: [
ConfigModule.forRoot(),
PgCacheModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
connectionString: configService.get<string>("DATABASE_URL"),
cleanupInterval: configService.get<number>("CACHE_CLEANUP_INTERVAL"),
table: "app_cache",
}),
}),
],
})
export class AppModule {}Using Existing Pool
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
});
@Module({
imports: [
PgCacheModule.forRoot({
pool,
cleanupInterval: 30000, // 30 seconds
}),
],
})
export class AppModule {}API
The PgCacheService provides the same methods as PgCache from @pgcache/core:
Methods
set<T>(key, value, options?)- Set a value with optional TTLsetNX<T>(key, value, options?)- Set a value only if key doesn't exist (returns boolean)get<T>(key)- Get a valuedel(key)- Delete a keydelIfEquals<T>(key, expectedValue)- Delete only if value matches (safe lock release)exists(key)- Check if key existsttl(key)- Get remaining TTLclear()- Clear all entrieskeys(pattern, caseInsensitive?)- Get keys matching patternmget<T>(keys)- Get multiple valuesmset<T>(entries)- Set multiple entriescleanup()- Manually cleanup expired entriesstats()- Get cache statisticsgetClient()- Get underlying PgCache instance
Configuration Options
interface PgCacheOptions {
connectionString?: string; // PostgreSQL connection string
pool?: Pool; // Existing pg Pool
poolConfig?: PoolConfig; // Pool configuration
cleanupInterval?: number; // Auto-cleanup interval (ms, default: 60000)
table?: string; // Table name (default: "pgcache")
autoInit?: boolean; // Auto-create table (default: true)
}Example: Caching Decorator
Create a custom caching decorator:
import { PgCacheService } from "@pgcache/nest";
export function Cacheable(ttl: number = 300) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cache: PgCacheService = this.cache;
const cacheKey = `${target.constructor.name}:${propertyKey}:${JSON.stringify(args)}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;
const result = await originalMethod.apply(this, args);
await cache.set(cacheKey, result, { ttl });
return result;
};
return descriptor;
};
}
// Usage
@Injectable()
export class ProductService {
constructor(private readonly cache: PgCacheService) {}
@Cacheable(600) // 10 minutes
async getProduct(id: string) {
return this.productRepository.findById(id);
}
}Testing
The module can be tested using @nestjs/testing:
import { Test } from "@nestjs/testing";
import { PgCacheModule, PgCacheService } from "@pgcache/nest";
describe("UserService", () => {
let service: UserService;
let cache: PgCacheService;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
PgCacheModule.forRoot({
connectionString: process.env.TEST_DATABASE_URL,
}),
],
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
cache = module.get<PgCacheService>(PgCacheService);
});
it("should cache user data", async () => {
// Your tests here
});
});License
MIT
