npm package: @origins-digital/cacheable
Description: cacheable package
Author: Origins Digital
Latest version: 3.1.4

# @origins-digital/cacheable

A powerful caching package for NestJS applications that provides decorators for Redis-based caching with automatic cache invalidation and key management.

## Installation

```bash
npm install @origins-digital/cacheable
```

## Features

- Redis-based caching with decorators
- Automatic cache key generation
- Cache invalidation support
- TTL (Time To Live) configuration
- Raw and formatted cache responses
- Context-aware caching
- Cache hit/miss tracking
- Automatic cache status headers
- Response transformation

## Usage

### Basic Setup

First, configure the Redis connection and response transformation in your module:

```typescript
import { Module } from '@nestjs/common';
import { CacheableModule } from '@origins-digital/cacheable';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ResponseTransformInterceptor } from '@origins-digital/cacheable';

@Module({
  imports: [
    CacheableModule.forRoot({
      host: 'localhost',
      port: 6379,
      // Optional Redis configuration
      // password: 'your-password',
      // db: 0,
    }),
  ],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: ResponseTransformInterceptor,
    },
  ],
})
export class AppModule {}
```

### Using ResponseTransformInterceptor

The `ResponseTransformInterceptor` automatically transforms responses and adds cache headers:

```typescript
import { Controller, Get } from '@nestjs/common';
import { Cacheable } from '@origins-digital/cacheable';
import { GenericOutput } from '@origins-digital/cacheable';

@Controller('users')
export class UserController {
  @Get(':id')
  @Cacheable({
    key: 'user:{id}',
    raw: false,
  })
  async getUser(@Param('id') id: string) {
    const user = await this.userService.findById(id);
    return new GenericOutput(user);
  }

  @Get()
  @Cacheable({
    key: 'users:active',
    ttlSeconds: 300,
    raw: false,
  })
  async getActiveUsers() {
    const users = await this.userService.findActive();
    return new GenericOutput(users);
  }
}
```

With the interceptor in place, responses will be automatically transformed:

```typescript
// Example response for GET /users/123
{
  "data": {
    "id": "123",
    "name": "John Doe",
    "email": "john@example.com"
  },
  "metadata": {
    "cache": "hit" // or "miss", "set", "no-cache"
  }
}

// Example response for GET /users
{
  "data": [
    {
      "id": "123",
      "name": "John Doe",
      "email": "john@example.com"
    },
    {
      "id": "124",
      "name": "Jane Doe",
      "email": "jane@example.com"
    }
  ],
  "metadata": {
    "cache": "miss",
    "count": 2
  }
}
```

The interceptor will also add the `X-Redis-Cache` header to all responses.

### Using Cacheable Decorator

The `@Cacheable` decorator checks for cached results and sets cache if not found:

```typescript
import { Injectable } from '@nestjs/common';
import { Cacheable } from '@origins-digital/cacheable';

@Injectable()
export class UserService {
  @Cacheable({
    key: 'user:{id}',
    ttlSeconds: 3600, // Cache for 1 hour
  })
  async getUser(id: string) {
    // This will only execute if the cache miss
    return this.userRepository.findById(id);
  }
}
```

### Using CacheGet Decorator

The `@CacheGet` decorator only checks for cached results without setting cache:

```typescript
import { Injectable } from '@nestjs/common';
import { CacheGet } from '@origins-digital/cacheable';

@Injectable()
export class ProductService {
  @CacheGet({
    key: 'product:{id}',
  })
  async getProduct(id: string) {
    // This will execute on cache miss
    return this.productRepository.findById(id);
  }
}
```

### Using CacheClear Decorator

The `@CacheClear` decorator invalidates cache entries:

```typescript
import { Injectable } from '@nestjs/common';
import { CacheClear } from '@origins-digital/cacheable';

@Injectable()
export class UserService {
  @CacheClear({
    key: 'user:{id}',
  })
  async updateUser(id: string, data: any) {
    // This will clear the cache for this user
    return this.userRepository.update(id, data);
  }
}
```

### Cache Options

All decorators accept options for fine-tuning cache behavior:

```typescript
interface CacheOptions {
  key?: string; // Cache key pattern
  ttlSeconds?: number; // Time to live in seconds
  excludeContext?: boolean; // Exclude method context from key
  forceOne?: boolean; // Force single item from array
  raw?: boolean; // Return raw data without metadata
}
```

### Cache Response Format

By default, cache responses include metadata:

```typescript
interface CacheResult {
  $fromCache: 'hit' | 'miss' | 'set';
  $responseBody: any;
}
```

### Using GenericOutput

To get a properly typed response, you can use the `GenericOutput` utility:

```typescript
import { Injectable } from '@nestjs/common';
import { Cacheable, GenericOutput } from '@origins-digital/cacheable';

interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable()
export class UserService {
  @Cacheable({
    key: 'user:{id}',
    ttlSeconds: 3600,
  })
  async getUser(id: string): Promise<GenericOutput<User>> {
    const user = await this.userRepository.findById(id);
  }
}

// Usage
const result = await userService.getUser('123');
const userData = GenericOutput.data(user); // Get the actual user data
const isFromCache = GenericOutput.fromCache(user); // Check if data came from cache
const isCacheable = GenericOutput.cacheable(user); // Check the cache status: hit, miss, no-cache, set
```

### Raw vs Non-Raw Usage Patterns

The `raw` option should be used differently depending on where the decorator is applied:

#### Service Level (raw: true)

When using `@Cacheable` at the service level, set `raw: true` to get the data directly:

```typescript
@Injectable()
export class UserService {
  @Cacheable({
    key: 'user:{id}',
    raw: true, // Get data directly at service level
  })
  async getUser(id: string): Promise<User> {
    return this.userRepository.findById(id);
  }
}
```

#### Controller Level (raw: false)

When using `@Cacheable` at the controller level, keep `raw: false` to enable cache status headers:

```typescript
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  @Cacheable({
    key: 'user:{id}',
    raw: false, // Keep metadata for headers
  })
  async getUser(@Param('id') id: string): Promise<CacheResult<User>> {
    return this.userService.getUser(id);
  }
}
```

The cache status will be automatically added to the response headers:

- `X-Redis-Cache: hit` - Data was found in cache
- `X-Redis-Cache: miss` - Data was not in cache
- `X-Redis-Cache: set` - Data was set in cache
- `X-Redis-Cache: no-cache` - Cache was not used

### Advanced Examples

#### Using forceOne and raw options

```typescript
import { Injectable } from '@nestjs/common';
import { Cacheable } from '@origins-digital/cacheable';

@Injectable()
export class UserService {
  // This will return a single user from cache or database
  @Cacheable({
    key: 'user:{id}',
    forceOne: true, // If the result is an array, return only the first item
    raw: true, // Return only the data without metadata
  })
  async getUser(id: string): Promise<User> {
    return this.userRepository.findById(id);
  }

  // This will return a list of users with metadata
  @Cacheable({
    key: 'users:active',
    ttlSeconds: 300,
  })
  async getActiveUsers(): Promise<CacheResult<User[]>> {
    return this.userRepository.findActive();
  }
}

// Usage
const user = await userService.getUser('123'); // Returns User directly
const activeUsers = await userService.getActiveUsers(); // Returns CacheResult<User[]>
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.