From 797ed241f10597aa096abc66088e9d2950220661 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Fri, 7 Jun 2024 17:32:26 -0300 Subject: [PATCH 1/5] chore: improve readme --- packages/nestjs-cache/README.md | 779 ++++++++++++++++++++++++++------ 1 file changed, 630 insertions(+), 149 deletions(-) diff --git a/packages/nestjs-cache/README.md b/packages/nestjs-cache/README.md index 6ceef7b9..24e8687c 100644 --- a/packages/nestjs-cache/README.md +++ b/packages/nestjs-cache/README.md @@ -1,90 +1,383 @@ -# Rockets NestJS Cache - -A module for managing a basic Cache entity, including controller with full CRUD, DTOs, sample data factory and seeder. - -## Project - -[![NPM Latest](https://img.shields.io/npm/v/@concepta/nestjs-user)](https://www.npmjs.com/package/@concepta/nestjs-user) -[![NPM Downloads](https://img.shields.io/npm/dw/@conceptadev/nestjs-user)](https://www.npmjs.com/package/@concepta/nestjs-user) -[![GH Last Commit](https://img.shields.io/github/last-commit/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets) -[![GH Contrib](https://img.shields.io/github/contributors/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets/graphs/contributors) -[![NestJS Dep](https://img.shields.io/github/package-json/dependency-version/conceptadev/rockets/@nestjs/common?label=NestJS&logo=nestjs&filename=packages%2Fnestjs-core%2Fpackage.json)](https://www.npmjs.com/package/@nestjs/common) +# Rockets NestJS Cache Documentation ## Table of Contents +- [Rockets NestJS Cache Documentation](#rockets-nestjs-cache-documentation) + - [Table of Contents](#table-of-contents) + - [Tutorials](#tutorials) + - [Getting Started with Rockets NestJS Cache](#getting-started-with-rockets-nestjs-cache) + - [Introduction](#introduction) + - [Installation](#installation) + - [Basic Setup in a NestJS Project](#basic-setup-in-a-nestjs-project) + - [Registering CacheModule Asynchronously for multiple entities](#registering-cachemodule-asynchronously-for-multiple-entities) + - [Reference](#reference) + - [CacheModule API Reference](#cachemodule-api-reference) + - [register(options: CacheOptions)](#registeroptions-cacheoptions) + - [registerAsync(options: CacheAsyncOptions)](#registerasyncoptions-cacheasyncoptions) + - [forRoot(options: CacheOptions)](#forrootoptions-cacheoptions) + - [forRootAsync(options: CacheAsyncOptions)](#forrootasyncoptions-cacheasyncoptions) + - [forFeature(options: CacheOptions)](#forfeatureoptions-cacheoptions) + - [CacheOptionsInterface](#cacheoptionsinterface) + - [CacheModule Classes and Interfaces](#cachemodule-classes-and-interfaces) + - [CacheEntityInterface](#cacheentityinterface) + - [CacheServiceInterface](#cacheserviceinterface) + - [How CacheOptionsInterface is Used in the Controller and Endpoints](#how-cacheoptionsinterface-is-used-in-the-controller-and-endpoints) + +## Tutorials + +### Getting Started with Rockets NestJS Cache + +#### Introduction + +The Rockets NestJS Cache module is designed to provide an easy and efficient way to manage cached data in your application. This tutorial will guide you through the initial steps to set up and use the Rockets NestJS Cache module. + +#### Installation -- [Introduction](#introduction) -- [Installation](#installation) -- [Usage](#usage) -- [Configuration Details](#configuration-details) -- [Setup](#setup) -- [Configuration Options Explained](#configuration-options-explained) - -## Introduction - -This module is a basic implementation of a caching mechanism for your application. It allows you to cache data in a database and retrieve it later, ensuring that your application is not repeatedly fetching the same data from a database. - -Installation To install the module, use the following command: -## Installation - -`yarn add @concepta/nestjs-cache` - -## Configuration - -The Cache Module allows for detailed configuration to link your application's data models with caching mechanisms. - -### Configuration Options Explained - -- **settings**: Manages how entities are assigned for caching. -- **entities**: Specifies which entities are to be cached. +```sh +yarn add typeorm +yarn add class-transformer +yarn add class-validator +yarn add @nestjs/typeorm +yarn add @concepta/nestjs-crud +yarn add @concepta/nestjs-typeorm-ext +yarn add @concepta/nestjs-cache +``` -## Usage +On this documentation we will use `sqlite3` as database, but you can use whatever you want -To utilize the caching module, you need to define the entities and configure the module appropriately. +```sh +yarn add sqlite3 +``` -## Example -Define your UserEntityFixture and UserCacheEntityFixture entities as follows: +#### Basic Setup in a NestJS Project + +1. **User Module**: Let's create a simple UserModule with Entity, Service, Controller and Module, to be used in our tutorial so we can cache user-related information with the cache module: + + ```typescript + import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; + import { UserCache } from '../user-cache/user-cache.entity'; + + @Entity() + export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @OneToMany(() => UserCache, (userCache) => userCache.assignee) + userCaches!: UserCache[]; + } + ``` + + ```typescript + import { Injectable } from '@nestjs/common'; + import { InjectRepository } from '@nestjs/typeorm'; + import { Repository } from 'typeorm'; + import { User } from './user.entity'; + + @Injectable() + export class UserService { + constructor( + @InjectRepository(User) + private userRepository: Repository, + ) {} + + findAll(): Promise { + return this.userRepository.find(); + } + + findOne(id: string): Promise { + return this.userRepository.findOne({ + where: { id }, + }); + } + + async create(userData: Partial): Promise { + const newUser = this.userRepository.create(userData); + await this.userRepository.save(newUser); + return newUser; + } + } + ``` + + ```typescript + import { Controller, Get, Post, Body, Param } from '@nestjs/common'; + import { UserService } from './user.service'; + import { User } from './user.entity'; + + @Controller('user') + export class UserController { + constructor(private readonly userService: UserService) {} + + @Get() + async findAll(): Promise { + return this.userService.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.userService.findOne(id); + } + + @Post() + async create(@Body() userData: Partial): Promise { + return this.userService.create(userData); + } + } + ``` + + ```typescript + import { Module } from '@nestjs/common'; + import { TypeOrmModule } from '@nestjs/typeorm'; + import { UserController } from './user.controller'; + import { UserService } from './user.service'; + import { User } from './user.entity'; + + @Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UserController], + providers: [UserService], + }) + export class UserModule {} + ``` + +2. **User Cache Module**: Let's create the entity `UserCache` and the `UserCacheModule` that imports our `CacheModule` passing all configurations needed, please note that `CacheSqliteEntity` and `CachePostgresEntity` are provided by the Rockets NestJS Cache module, so you can use them to create your cache entity, and they have a unique index with following properties `'key', 'type', 'assignee.id'`: + + ```typescript + import { Entity, ManyToOne } from 'typeorm'; + import { User } from '../user/user.entity'; + import { CacheSqliteEntity } from '@concepta/nestjs-cache'; + import { ReferenceIdInterface } from '@concepta/ts-core'; + + @Entity() + export class UserCache extends CacheSqliteEntity { + @ManyToOne(() => User, (user) => user.userCaches) + assignee!: ReferenceIdInterface; + } + ``` + + ```typescript + import { Module } from '@nestjs/common'; + import { TypeOrmModule } from '@nestjs/typeorm'; + import { CrudModule } from '@concepta/nestjs-crud'; + import { CacheModule } from '@concepta/nestjs-cache'; + import { User } from '../user/user.entity'; + import { UserCache } from './user-cache.entity'; + + @Module({ + imports: [ + TypeOrmModule.forFeature([User]), + CacheModule.register({ + entities: { + userCache: { + entity: UserCache, + }, + }, + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + }), + CrudModule.forRoot({}), + ], + }) + export class UserCacheModule {} + + ``` + +3. **App Module**:And let's create our app module to connect everything. + + ```ts + import { Module } from '@nestjs/common'; + import { UserCacheModule } from './user-cache/user-cache.module'; + import { UserModule } from './user/user.module'; + import { User } from './user/user.entity'; + import { UserCache } from './user-cache/user-cache.entity'; + import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; + + @Module({ + imports: [ + TypeOrmExtModule.forRoot({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + entities: [User, UserCache], + }), + UserCacheModule, + UserModule, + ], + controllers: [], + providers: [], + }) + export class AppModule {} + ``` + +#### Using the RestFull endpoints to access cache + +After setting up the basic configuration, you can start using the caching functionality in your application. ```ts -@Entity() -export class UserEntityFixture implements ReferenceIdInterface { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ default: false }) - isActive!: boolean; - - @OneToMany(() => UserCacheEntityFixture, (userCache) => userCache.assignee) - userCaches!: UserCacheEntityFixture[]; -} - + assignments: { + user: { entityKey: 'userCache' }, + }, ``` -```typescript -@Entity() -export class UserCacheEntityFixture extends CacheSqliteEntity { - @ManyToOne(() => UserEntityFixture, (user) => user.userCaches) - assignee!: ReferenceIdInterface; -} -``` -## Setup - -### Define Entities -Ensure that you have defined the entities in your project that you wish to cache. For instance, a UserEntityFixture might be used for storing user information. +The code above will generate a route for the client to have access, the module will generate the following endpoint `/cache/user`, this endpoint will be referencing whatever entity was associated in the entities section, as you can see below. -### Configure the Cache Module -Incorporate the Cache Module into your application module and configure it for your specific needs: +``` +entities: { + userCache: { + entity: UserCacheEntityFixture, + }, +}, +``` +This will make the following endpoints available: + +1. **Create (POST)**: To create a new cache entry, the request body should match the `CacheCreatableInterface`: + + ``` + export interface CacheCreatableInterface extends Pick { + expiresIn: string | null; + } + ``` + + Example curl command: + + ``` + curl -X POST http://your-api-url/cache/user \ + -H "Content-Type: application/json" \ + -d '{ + "key": "exampleKey", + "type": "exampleType", + "data": "{data: 'example'}", + "assignee": { id: 'exampleId'}, + "expiresIn": "1h" + }' + ``` + +2. **Read (GET)**: To read a cache entry by its ID: + + ``` + curl -X GET http://your-api-url/cache/user/{id} + ``` + +3. **Update (PUT)**: To update an existing cache entry, the request body should match the `CacheUpdatableInterface`: + + ``` + export interface CacheUpdatableInterface extends Pick { + expiresIn: string | null; + } + ``` + + Example curl command: + + ``` + curl -X PUT http://your-api-url/cache/user/{id} \ + -H "Content-Type: application/json" \ + -d '{ + "key": "updatedKey", + "type": "updatedType", + "data": "updatedData", + "assignee": "updatedAssignee", + "expiresIn": "2d" + }' + ``` + +4. **Delete (DELETE)**: To delete a cache entry by its ID: + + ``` + curl -X DELETE http://your-api-url/cache/user/{id} + ``` + +Replace `http://your-api-url` with the actual base URL of your API, and `{id}` with the actual ID of the cache entry you wish to interact with. + + +5. **Testing the cache**: You can test the cache by creating a new user and then accessing the cache endpoint: + + ```bash + curl -X POST http://your-api-url/user \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + }' + ``` + The response will be something like this: + ```json + { + "name": "John Doe", + "id": "5f84d150-7ebd-4c59-997a-df65a5935123" + } + ``` + + Now, let's add something to the cache with reference of the user + + ```bash + curl -X POST http://your-api-url/cache/user \ + -H "Content-Type: application/json" \ + -d '{ + "key": "user", + "type": "filter", + "data": "{data: 'example'}", + "assignee": { "id": "5f84d150-7ebd-4c59-997a-df65a5935123"}, + "expiresIn": "1h" + }' + ``` + + It will give you a response similar to this. + + ```json + { + "id": "a70e629b-7e6d-4dcc-9e74-a2e376f1c19a", + "dateCreated": "2024-06-07T15:16:56.000Z", + "dateUpdated": "2024-06-07T15:16:56.000Z", + "dateDeleted": null, + "version": 1, + "key": "user", + "data": "{data: 'example'}", + "type": "filter", + "assignee": { + "id": "0e5bee5d-5d53-46ef-a94a-22aceea81fc5" + } + } + ``` + + Now, if you access the cache endpoint `/cache/user`, you will see the new user cached: + + ```bash + curl -X GET http://your-api-url/cache/user + ``` + ```json + [ + { + "id": "24864a7e-372e-4426-97f0-7e1c7514be16", + "dateCreated": "2024-06-07T15:47:38.000Z", + "dateUpdated": "2024-06-07T15:47:38.000Z", + "dateDeleted": null, + "version": 1, + "key": "user", + "data": "{data: 'example'}", + "type": "filter", + "assignee": { + "id": "5f84d150-7ebd-4c59-997a-df65a5935123" + } + } + ] + ``` + +# How-to Guides + +## Registering CacheModule Synchronously + +To register the CacheModule synchronously, you can use the `register` method. This method allows you to pass configuration options directly. + +### Example: ```ts -// ... - @Module({ imports: [ - TypeOrmExtModule.forRoot({ - type: 'postgres', - url: 'postgres://user:pass@localhost:5432/postgres', - }), CacheModule.register({ settings: { assignments: { @@ -97,131 +390,319 @@ Incorporate the Cache Module into your application module and configure it for y }, }, }), - CacheModule.forRoot({}), ], }) export class AppModule {} ``` +## Registering CacheModule Asynchronously -### Client-side Interaction with CRUD Endpoints +For more advanced use cases, you can register the CacheModule asynchronously using the `registerAsync` method. This method is useful when you need to perform asynchronous operations to get the configuration options. -The Cache API provides RESTful endpoints for managing cached data. The endpoint path follows the format `cache/:assignment`, where `:assignment` is defined in your module configuration. For example, if you have the configuration: +### Example: -```typescript -CacheModule.register({ - settings: { - assignments: { - user: { entityKey: 'userCache' }, - }, - }, - entities: { - userCache: { - entity: UserCacheEntityFixture, - }, - }, +```ts +@Module({ + imports: [ + CacheModule.registerAsync({ + useFactory: async () => ({ + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + }, + }), + }), + ], }) +export class AppModule {} ``` -The endpoint will be cache/user. Below are examples of how to interact with the API using curl. +## Global Registering CacheModule Asynchronously -### Create (POST) -To create a new cache entry, the request body should match the CacheCreatableInterface: +For more advanced use cases, you can register the global CacheModule asynchronously using the `forRootAsync` method. This method is useful when you need to perform asynchronous operations to get the configuration options. +### Example: ```ts -export interface CacheCreatableInterface extends Pick { - expiresIn: string | null; -} +@Module({ + imports: [ + CacheModule.forRootAsync({ + useFactory: async () => ({ + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + }, + }), + }), + ], +}) +export class AppModule {} ``` -Example curl command: +## Registering CacheModule Asynchronously for multiple entities -```sh -curl -X POST http://your-api-url/cache/user \ --H "Content-Type: application/json" \ --d '{ - "key": "exampleKey", - "type": "exampleType", - "data": "{data: 'example'}", - "assignee": { id: 'exampleId'}, - "expiresIn": "1h" -}' +This section demonstrates how to register the CacheModule asynchronously when dealing with multiple entities. + +### Example: +```ts +@Module({ + imports: [ + CacheModule.registerAsync({ + useFactory: async () => ({ + settings: { + assignments: { + user: { entityKey: 'userCache' }, + pet: { entityKey: 'petCache' }, + }, + }, + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + petCache: { + entity: PetCacheEntity, + }, + }, + }), + }), + ], +}) +export class AppModule {} +``` + ## Using the CacheService to access cache + +The `CacheService` provided by the Rockets NestJS Cache module offers a comprehensive set of methods to manage cache entries programmatically from the API side. This service allows for creating, updating, retrieving, and deleting cache entries, as well as managing cache entries for specific assignees. Below is an overview of how to use these services in your application. + +##### Creating a Cache Entry + +To create a new cache entry, you can use the `create` method of the `CacheService`. This method requires specifying the cache assignment, the cache data, and optionally, query options. + +### CacheService Methods Documentation +CacheService is exported in the CacheModule, so +Below is a simple documentation for each method in the `CacheService` class, including examples of how to use them. + +#### 1. `create(assignment, cache, queryOptions)` +Creates a new cache entry. + +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The data to create, implementing `CacheCreatableInterface`. +- `queryOptions`: Optional. Additional options for the query. + +**Example:** +Create a cache entry with a unique combination of `key`, `type`, and `assignee.id`: + +```ts + await cacheService.create('userCache', { + key: 'userSession', + type: 'session', + data: { sessionData: 'abc123' }, + assignee: { id: 'user1' }, + expiresIn: '24h' + }); ``` -### Read (GET) -To read a cache entry by its ID: -```sh -curl -X GET http://your-api-url/cache/user/{id} +#### 2. `update(assignment, cache, queryOptions)` +Updates an existing cache entry. + +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The data to update, implementing `CacheUpdatableInterface`. +- `queryOptions`: Optional. Additional options for the query. + +**Example:** +Update a cache entry identified by `key`, `type`, and `assignee.id`: + +```ts +await cacheService.update('userCache', { + key: 'userSession', + type: 'session', + data: { sessionData: 'updated123' }, + assignee: { id: 'user1' } +}); ``` -### Update (PUT) -To update an existing cache entry, the request body should match the CacheUpdatableInterface: + +#### 3. `delete(assignment, cache, queryOptions)` +Deletes a cache entry. + +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The cache to delete, specifying `key`, `type`, and `assignee`. + +**Example:** +Delete a cache entry using `key`, `type`, and `assignee.id`: ```ts -export interface CacheUpdatableInterface extends Pick { - expiresIn: string | null; -} +await cacheService.delete('userCache', { + key: 'userSession', + type: 'session', + assignee: { id: 'user1' } +}); +``` + +#### 4. `getAssignedCaches(assignment, cache, queryOptions)` +Retrieves all cache entries for a given assignee. + +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The cache to get assignments, specifying `assignee`. + +**Example:** +Retrieve all caches for a specific assignee: + +```ts +const caches = await cacheService.getAssignedCaches('userCache', { assignee: { id: 'userId' } }); ``` -Example curl command: -```sh -curl -X PUT http://your-api-url/cache/user/{id} \ --H "Content-Type: application/json" \ --d '{ - "key": "updatedKey", - "type": "updatedType", - "data": "updatedData", - "assignee": "updatedAssignee", - "expiresIn": "2d" -}' +#### 5. `get(assignment, cache, queryOptions)` +Retrieves a specific cache entry. +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The cache to get, specifying `key`, `type`, and `assignee`. + +**Example:** +Retrieve a specific cache entry using `key`, `type`, and `assignee.id`: + +```ts +const cacheEntry = await cacheService.get('userCache', { + key: 'userSession', + type: 'session', + assignee: { id: 'user1' } +}); ``` -### Delete (DELETE) -To delete a cache entry by its ID: -```sh -curl -X DELETE http://your-api-url/cache/user/{id} + +#### 6. `clear(assignment, cache, queryOptions)` +Clears all caches for a given assignee. + +**Parameters:** +- `assignment`: The cache assignment. +- `cache`: The cache to clear, specifying `assignee`. + +**Example:** +Clear all caches for a specific assignee: + +```ts +await cacheService.clear('userCache', { assignee: { id: 'user1' } }); + ``` -Replace http://your-api-url with the actual base URL of your API, and {id} with the actual ID of the cache entry you wish to interact with. +These methods provide a comprehensive interface for managing cache entries in a NestJS application using the `CacheService`. Each method supports optional query options for more granular control over the database operations. + +# Reference + +Detailed Descriptions of All Classes, Methods, and Properties + +### CacheModule API Reference + +#### register(options: CacheOptions) +Registers the module with synchronous options. + +#### registerAsync(options: CacheAsyncOptions) +Registers the module with asynchronous options. + +#### forRoot(options: CacheOptions) +Registers the module globally with synchronous options. + +#### forRootAsync(options: CacheAsyncOptions) +Registers the module globally with asynchronous options. + +#### forFeature(options: CacheOptions) +Registers the module for specific features with custom options. + +### CacheOptionsInterface + +The CacheOptionsInterface provides various configuration options to customize the behavior of the CacheModule. Below is a summary of the key options: + +#### settings (required) +Manages how entities are assigned for caching. It includes properties like: +- `assignments`: An object mapping assignments to entity keys. the name defined here will be the route for client to access and it should be used on entities as well to match with the entity class +- `expiresIn`: Optional property defining the default expiration time for cache entries. + +#### entities (required) +Specifies which entities are to be cached. It includes properties like: +- `entities`: A record of entities and their corresponding configuration options. + +### CacheModule Classes and Interfaces + +#### CacheEntityInterface +Defines the structure of a cache entity. This interface includes properties such as: +- `id`: Unique identifier for the cache entry. +- `key`: Key used to reference the cache entry. +- `type`: Type of data being cached. +- `data`: The actual data being cached. +- `assignee`: The entity associated with the cache entry. +- `expiresIn`: The expiration time for the cache entry. + +#### CacheServiceInterface +Provides methods for interacting with the cache service. This interface includes methods such as: +- `create`: Method to create a new cache entry. +- `findOne`: Method to find a cache entry by its ID. +- `update`: Method to update an existing cache entry. +- `remove`: Method to remove a cache entry. + +### How CacheOptionsInterface is Used in the Controller and Endpoints + +The `CacheSettingsInterface` and `CacheOptionsInterface` are used to configure the caching behavior in the `CacheCrudController`. The `CacheCrudController` provides endpoints for CRUD operations on cache entries and uses these interfaces to manage the settings and services for each cache assignment. -By following these examples, you can perform Create, Read, Update, and Delete operations on your cache data using the provided endpoints. +- `CacheSettingsInterface` manages how entities are assigned for caching and specifies the expiration time for cache entries. It is used to ensure the correct service and configuration are applied to each cache assignment. +- `CacheOptionsInterface` includes the settings for managing cache assignments and expiration times. It is used to register and configure the CacheModule, determining which entities should be cached and how they should be handled. -## How the Module Works +By using these interfaces, the `CacheCrudController` can dynamically handle different cache assignments and provide a consistent caching strategy across the application. The endpoints in the controller allow for creating, reading, updating, and deleting cache entries, ensuring that the caching behavior is flexible and easily configurable. -### Overview +## Explanation -The Rockets NestJS Cache module is designed to provide an easy and efficient way to manage cached data in your application. Here's a simple explanation of how it works: +### Conceptual Overview of Caching -### Dynamic Controller Generation +#### What is Caching? -The module automatically creates controllers based on the settings you provide. This means you can set up different cache entities and their endpoints without writing extra code. The endpoints are created based on the assignments you define in the module configuration. +Caching is a technique used to store copies of data in a temporary storage location (cache) so that future requests for that data can be served faster. It helps in reducing the time required to access data and decreases the load on the primary data source. -### CRUD Operations +#### Benefits of Using Cache -The module supports all basic operations: Create, Read, Update, and Delete (CRUD). These operations are managed by a controller that uses special decorators to define and control access. The operations are routed based on the cache assignment specified in the request. +- **Improved Performance**: By serving data from the cache, applications can respond to requests faster than retrieving the data from the primary source each time. +- **Reduced Latency**: Caching reduces the latency involved in data retrieval operations, improving the user experience. +- **Lower Database Load**: By reducing the number of direct database queries, caching helps in decreasing the load on the database, leading to better overall performance. +- **Scalability**: Caching allows applications to handle higher loads by serving frequent requests from the cache instead of the database. -### Service Injection +#### Why Use NestJS Cache? -The module uses a technique called dependency injection to manage its settings and services. It injects configuration settings that define cache assignments and expiration times, as well as a list of CRUD services. This allows the module to dynamically choose the right service for each cache assignment. +NestJS Cache provides a powerful and flexible caching solution that integrates seamlessly with the NestJS framework and stores your cache on the database, so you can reuse it in any other part of your application or even in other applications that are calling your API. It allows developers to manage cached data efficiently and provides built-in support for CRUD operations on cache entries. Here are some key reasons to use NestJS Cache: -### Handling Assignments +- **Integration with NestJS Ecosystem**: The module integrates well with other NestJS modules and leverages the framework's features, such as decorators and dependency injection. +- **Customizable and Extensible**: It allows for customization through various configuration options and can be extended with custom services and guards. +- **Ease of Use**: The module provides straightforward APIs for managing cache entries, making it easy to implement caching in your application. +- **Automatic Expiration Handling**: The module can automatically handle expiration times for cache entries, ensuring that stale data is not served. -In the module configuration, you specify different cache assignments. Each assignment is linked to a specific entity and CRUD service. When a request is made to the cache endpoint, the module uses the assignment in the request to determine which entity and service to use. +#### When to Use NestJS Cache -### Exception Handling +NestJS Cache is useful in scenarios where data is frequently accessed but does not change often. It is also beneficial when the performance of data retrieval operations needs to be improved. Here are some examples of when to use NestJS Cache: -The module has built-in error handling to manage invalid assignments or entity keys. It throws specific errors when something goes wrong, ensuring that your application can handle these issues gracefully and provide clear error messages to the client. +- **Storing Filters for a Specific Dashboard**: If you have a dashboard with complex filters that are expensive to compute, you can cache the filters for each user. This way, subsequent requests can be served from the cache, reducing the load on the server and improving response times. -### Summary +Example: +When a user applies a filter on a dashboard, the filter settings can be cached. The next time the user accesses the dashboard, the cached filter can be retrieved quickly without recomputing it. -The Rockets NestJS Cache module provides a powerful and flexible way to manage cached data. By automatically generating controllers, using dependency injection, and handling various types of cached data dynamically, it ensures your application can run efficiently without redundant database queries. +#### Design Choices in CacheModule +##### Synchronous vs Asynchronous Registration -#### ENV +- **Synchronous Registration**: This method is used when all configuration options are available at the time of module registration. It is simple and straightforward, making it suitable for most use cases. +- **Asynchronous Registration**: This method is used when configuration options need to be fetched or computed asynchronously. It provides greater flexibility and is useful for advanced scenarios where configuration depends on runtime conditions. -Configurations available via environment. +##### Global vs Feature-Specific Registration -| Variable | Type | Default | | -| -------------------------- | ---------- | ------- | ------------------------------------ | -| `CACHE_MODULE_SEEDER_AMOUNT` | `` | `50` | number of additional users to create | -| `CACHE_EXPIRE_IN` | `` | `1d` | string for the amount of time to expire the cache | +- **Global Registration**: Registers the CacheModule at the root level, making it available throughout the entire application. It is useful for shared configurations that need to be applied universally. +- **Feature-Specific Registration**: Registers the CacheModule for specific features or modules, allowing for fine-grained control over caching behavior. It is useful when different parts of the application have unique caching requirements. From 8895b089b061fe3779a63d52d447c0b3e9a291af Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Tue, 11 Jun 2024 17:48:23 -0300 Subject: [PATCH 2/5] chore: update readme with documentation --- packages/nestjs-cache/README.md | 705 ++++++++++++++++++-------------- 1 file changed, 397 insertions(+), 308 deletions(-) diff --git a/packages/nestjs-cache/README.md b/packages/nestjs-cache/README.md index 24e8687c..0bf0e98e 100644 --- a/packages/nestjs-cache/README.md +++ b/packages/nestjs-cache/README.md @@ -28,13 +28,25 @@ #### Introduction -The Rockets NestJS Cache module is designed to provide an easy and efficient way to manage cached data in your application. This tutorial will guide you through the initial steps to set up and use the Rockets NestJS Cache module. +The Rockets NestJS Cache module is designed to provide an easy and efficient way +to manage cached data in your application. This tutorial will guide you through +the initial steps to set up and use the Rockets NestJS Cache module. #### Installation To install the module, use the following command: ```sh +npm install typeorm +npm install class-transformer +npm install class-validator +npm install @nestjs/typeorm +npm install @concepta/nestjs-crud +npm install @concepta/nestjs-typeorm-ext +npm install @concepta/nestjs-cache + +or + yarn add typeorm yarn add class-transformer yarn add class-validator @@ -44,7 +56,8 @@ yarn add @concepta/nestjs-typeorm-ext yarn add @concepta/nestjs-cache ``` -On this documentation we will use `sqlite3` as database, but you can use whatever you want +On this documentation we will use `sqlite3` as database, but you can use +whatever you want ```sh yarn add sqlite3 @@ -52,182 +65,191 @@ yarn add sqlite3 #### Basic Setup in a NestJS Project -1. **User Module**: Let's create a simple UserModule with Entity, Service, Controller and Module, to be used in our tutorial so we can cache user-related information with the cache module: +1. **User Module**: Let's create a simple UserModule with Entity, Service, + Controller, and Module, to be used in our tutorial so we can cache + user-related information with the cache module: - ```typescript - import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; - import { UserCache } from '../user-cache/user-cache.entity'; +```typescript +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { UserCache } from '../user-cache/user-cache.entity'; - @Entity() - export class User { - @PrimaryGeneratedColumn('uuid') - id: string; +@Entity() +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; - @Column() - name: string; + @Column() + name: string; - @OneToMany(() => UserCache, (userCache) => userCache.assignee) - userCaches!: UserCache[]; - } - ``` - - ```typescript - import { Injectable } from '@nestjs/common'; - import { InjectRepository } from '@nestjs/typeorm'; - import { Repository } from 'typeorm'; - import { User } from './user.entity'; - - @Injectable() - export class UserService { - constructor( - @InjectRepository(User) - private userRepository: Repository, - ) {} - - findAll(): Promise { - return this.userRepository.find(); - } - - findOne(id: string): Promise { - return this.userRepository.findOne({ - where: { id }, - }); - } - - async create(userData: Partial): Promise { - const newUser = this.userRepository.create(userData); - await this.userRepository.save(newUser); - return newUser; - } - } - ``` - - ```typescript - import { Controller, Get, Post, Body, Param } from '@nestjs/common'; - import { UserService } from './user.service'; - import { User } from './user.entity'; - - @Controller('user') - export class UserController { - constructor(private readonly userService: UserService) {} - - @Get() - async findAll(): Promise { - return this.userService.findAll(); - } - - @Get(':id') - async findOne(@Param('id') id: string): Promise { - return this.userService.findOne(id); - } - - @Post() - async create(@Body() userData: Partial): Promise { - return this.userService.create(userData); - } - } - ``` - - ```typescript - import { Module } from '@nestjs/common'; - import { TypeOrmModule } from '@nestjs/typeorm'; - import { UserController } from './user.controller'; - import { UserService } from './user.service'; - import { User } from './user.entity'; - - @Module({ - imports: [TypeOrmModule.forFeature([User])], - controllers: [UserController], - providers: [UserService], - }) - export class UserModule {} - ``` - -2. **User Cache Module**: Let's create the entity `UserCache` and the `UserCacheModule` that imports our `CacheModule` passing all configurations needed, please note that `CacheSqliteEntity` and `CachePostgresEntity` are provided by the Rockets NestJS Cache module, so you can use them to create your cache entity, and they have a unique index with following properties `'key', 'type', 'assignee.id'`: - - ```typescript - import { Entity, ManyToOne } from 'typeorm'; - import { User } from '../user/user.entity'; - import { CacheSqliteEntity } from '@concepta/nestjs-cache'; - import { ReferenceIdInterface } from '@concepta/ts-core'; - - @Entity() - export class UserCache extends CacheSqliteEntity { - @ManyToOne(() => User, (user) => user.userCaches) - assignee!: ReferenceIdInterface; + @OneToMany(() => UserCache, (userCache) => userCache.assignee) + userCaches!: UserCache[]; +} +``` + +```typescript +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from './user.entity'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private userRepository: Repository, + ) {} + + findAll(): Promise { + return this.userRepository.find(); } - ``` - - ```typescript - import { Module } from '@nestjs/common'; - import { TypeOrmModule } from '@nestjs/typeorm'; - import { CrudModule } from '@concepta/nestjs-crud'; - import { CacheModule } from '@concepta/nestjs-cache'; - import { User } from '../user/user.entity'; - import { UserCache } from './user-cache.entity'; - - @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - CacheModule.register({ - entities: { - userCache: { - entity: UserCache, - }, - }, - settings: { - assignments: { - user: { entityKey: 'userCache' }, - }, - }, - }), - CrudModule.forRoot({}), - ], - }) - export class UserCacheModule {} - ``` + findOne(id: string): Promise { + return this.userRepository.findOne({ + where: { id }, + }); + } -3. **App Module**:And let's create our app module to connect everything. + async create(userData: Partial): Promise { + const newUser = this.userRepository.create(userData); + await this.userRepository.save(newUser); + return newUser; + } +} +``` - ```ts - import { Module } from '@nestjs/common'; - import { UserCacheModule } from './user-cache/user-cache.module'; - import { UserModule } from './user/user.module'; - import { User } from './user/user.entity'; - import { UserCache } from './user-cache/user-cache.entity'; - import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; - - @Module({ - imports: [ - TypeOrmExtModule.forRoot({ - type: 'sqlite', - database: ':memory:', - synchronize: true, - entities: [User, UserCache], - }), - UserCacheModule, - UserModule, - ], - controllers: [], - providers: [], - }) - export class AppModule {} - ``` +```typescript +import { Controller, Get, Post, Body, Param } from '@nestjs/common'; +import { UserService } from './user.service'; +import { User } from './user.entity'; -#### Using the RestFull endpoints to access cache +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + + @Get() + async findAll(): Promise { + return this.userService.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.userService.findOne(id); + } + + @Post() + async create(@Body() userData: Partial): Promise { + return this.userService.create(userData); + } +} +``` + +```typescript +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; +import { User } from './user.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + controllers: [UserController], + providers: [UserService], +}) +export class UserModule {} +``` + +2. **User Cache Module**: Let's create the entity `UserCache` and the + `UserCacheModule` that imports our `CacheModule` passing all configurations + needed. Please note that `CacheSqliteEntity` and `CachePostgresEntity` are + provided by the Rockets NestJS Cache module, so you can use them to create + your cache entity. They have a unique index with the following properties: + `'key', 'type', 'assignee.id'`: + +```typescript +import { Entity, ManyToOne } from 'typeorm'; +import { User } from '../user/user.entity'; +import { CacheSqliteEntity } from '@concepta/nestjs-cache'; +import { ReferenceIdInterface } from '@concepta/ts-core'; + +@Entity() +export class UserCache extends CacheSqliteEntity { + @ManyToOne(() => User, (user) => user.userCaches) + assignee!: ReferenceIdInterface; +} +``` + +```typescript +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CrudModule } from '@concepta/nestjs-crud'; +import { CacheModule } from '@concepta/nestjs-cache'; +import { User } from '../user/user.entity'; +import { UserCache } from './user-cache.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + CacheModule.register({ + entities: { + userCache: { + entity: UserCache, + }, + }, + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + }), + CrudModule.forRoot({}), + ], +}) +export class UserCacheModule {} +``` -After setting up the basic configuration, you can start using the caching functionality in your application. +3. **App Module**:And let's create our app module to connect everything. ```ts - assignments: { - user: { entityKey: 'userCache' }, - }, +import { Module } from '@nestjs/common'; +import { UserCacheModule } from './user-cache/user-cache.module'; +import { UserModule } from './user/user.module'; +import { User } from './user/user.entity'; +import { UserCache } from './user-cache/user-cache.entity'; +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; + +@Module({ +imports: [ + TypeOrmExtModule.forRoot({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + entities: [User, UserCache], + }), + UserCacheModule, + UserModule, +], +controllers: [], +providers: [], +}) +export class AppModule {} ``` -The code above will generate a route for the client to have access, the module will generate the following endpoint `/cache/user`, this endpoint will be referencing whatever entity was associated in the entities section, as you can see below. +#### Using the RestFull endpoints to access cache + +After setting up the basic configuration, you can start using the caching +functionality in your application. +```ts +assignments: { + user: { entityKey: 'userCache' }, +}, ``` + +The code above will generate a route for the client to have access, the module +will generate the following endpoint `/cache/user`. This endpoint will be +referencing whatever entity was associated in the entities section, as you can +see below. +```ts entities: { userCache: { entity: UserCacheEntityFixture, @@ -237,142 +259,148 @@ entities: { This will make the following endpoints available: -1. **Create (POST)**: To create a new cache entry, the request body should match the `CacheCreatableInterface`: +1. **Create (POST)**: To create a new cache entry, the request body should +match the `CacheCreatableInterface`: - ``` - export interface CacheCreatableInterface extends Pick { - expiresIn: string | null; - } - ``` +```ts +export interface CacheCreatableInterface extends Pick { + expiresIn: string | null; +} +``` Example curl command: - ``` - curl -X POST http://your-api-url/cache/user \ - -H "Content-Type: application/json" \ - -d '{ - "key": "exampleKey", - "type": "exampleType", - "data": "{data: 'example'}", - "assignee": { id: 'exampleId'}, - "expiresIn": "1h" - }' - ``` +```sh +curl -X POST http://your-api-url/cache/user \ +-H "Content-Type: application/json" \ +-d '{ + "key": "exampleKey", + "type": "exampleType", + "data": "{data: 'example'}", + "assignee": { id: 'exampleId'}, + "expiresIn": "1h" +}' +``` 2. **Read (GET)**: To read a cache entry by its ID: - ``` - curl -X GET http://your-api-url/cache/user/{id} - ``` +```sh +curl -X GET http://your-api-url/cache/user/{id} +``` -3. **Update (PUT)**: To update an existing cache entry, the request body should match the `CacheUpdatableInterface`: +3. **Update (PUT)**: To update an existing cache entry, the request body should +match the `CacheUpdatableInterface`: - ``` - export interface CacheUpdatableInterface extends Pick { - expiresIn: string | null; - } - ``` +```sh +export interface CacheUpdatableInterface extends Pick { + expiresIn: string | null; +} +``` - Example curl command: +Example curl command: - ``` - curl -X PUT http://your-api-url/cache/user/{id} \ - -H "Content-Type: application/json" \ - -d '{ - "key": "updatedKey", - "type": "updatedType", - "data": "updatedData", - "assignee": "updatedAssignee", - "expiresIn": "2d" - }' - ``` +```sh +curl -X PUT http://your-api-url/cache/user/{id} \ +-H "Content-Type: application/json" \ +-d '{ + "key": "updatedKey", + "type": "updatedType", + "data": "updatedData", + "assignee": "updatedAssignee", + "expiresIn": "2d" +}' +``` 4. **Delete (DELETE)**: To delete a cache entry by its ID: - ``` - curl -X DELETE http://your-api-url/cache/user/{id} - ``` +```sh +curl -X DELETE http://your-api-url/cache/user/{id} +``` + +Replace `http://your-api-url` with the actual base URL of your API, and `{id}` +with the actual ID of the cache entry you wish to interact with. + -Replace `http://your-api-url` with the actual base URL of your API, and `{id}` with the actual ID of the cache entry you wish to interact with. +5. **Testing the cache**: You can test the cache by creating a new user and +then accessing the cache endpoint: +```bash +curl -X POST http://your-api-url/user \ +-H "Content-Type: application/json" \ +-d '{ + "name": "John Doe", +}' +``` +The response will be something like this: +```json +{ + "name": "John Doe", + "id": "5f84d150-7ebd-4c59-997a-df65a5935123" +} +``` -5. **Testing the cache**: You can test the cache by creating a new user and then accessing the cache endpoint: +Now, let's add something to the cache with reference of the user + +```bash +curl -X POST http://your-api-url/cache/user \ +-H "Content-Type: application/json" \ +-d '{ + "key": "user", + "type": "filter", + "data": "{data: 'example'}", + "assignee": { "id": "5f84d150-7ebd-4c59-997a-df65a5935123"}, + "expiresIn": "1h" +}' +``` - ```bash - curl -X POST http://your-api-url/user \ - -H "Content-Type: application/json" \ - -d '{ - "name": "John Doe", - }' - ``` - The response will be something like this: - ```json - { - "name": "John Doe", - "id": "5f84d150-7ebd-4c59-997a-df65a5935123" +It will give you a response similar to this. + +```json +{ + "id": "a70e629b-7e6d-4dcc-9e74-a2e376f1c19a", + "dateCreated": "2024-06-07T15:16:56.000Z", + "dateUpdated": "2024-06-07T15:16:56.000Z", + "dateDeleted": null, + "version": 1, + "key": "user", + "data": "{data: 'example'}", + "type": "filter", + "assignee": { + "id": "0e5bee5d-5d53-46ef-a94a-22aceea81fc5" } - ``` - - Now, let's add something to the cache with reference of the user - - ```bash - curl -X POST http://your-api-url/cache/user \ - -H "Content-Type: application/json" \ - -d '{ +} +``` + +Now, if you access the cache endpoint `/cache/user`, you will see the new user +cached: + +```bash + curl -X GET http://your-api-url/cache/user +``` +```json +[ + { + "id": "24864a7e-372e-4426-97f0-7e1c7514be16", + "dateCreated": "2024-06-07T15:47:38.000Z", + "dateUpdated": "2024-06-07T15:47:38.000Z", + "dateDeleted": null, + "version": 1, "key": "user", - "type": "filter", "data": "{data: 'example'}", - "assignee": { "id": "5f84d150-7ebd-4c59-997a-df65a5935123"}, - "expiresIn": "1h" - }' - ``` - - It will give you a response similar to this. - - ```json - { - "id": "a70e629b-7e6d-4dcc-9e74-a2e376f1c19a", - "dateCreated": "2024-06-07T15:16:56.000Z", - "dateUpdated": "2024-06-07T15:16:56.000Z", - "dateDeleted": null, - "version": 1, - "key": "user", - "data": "{data: 'example'}", - "type": "filter", - "assignee": { - "id": "0e5bee5d-5d53-46ef-a94a-22aceea81fc5" - } - } - ``` - - Now, if you access the cache endpoint `/cache/user`, you will see the new user cached: - - ```bash - curl -X GET http://your-api-url/cache/user - ``` - ```json - [ - { - "id": "24864a7e-372e-4426-97f0-7e1c7514be16", - "dateCreated": "2024-06-07T15:47:38.000Z", - "dateUpdated": "2024-06-07T15:47:38.000Z", - "dateDeleted": null, - "version": 1, - "key": "user", - "data": "{data: 'example'}", - "type": "filter", - "assignee": { - "id": "5f84d150-7ebd-4c59-997a-df65a5935123" - } + "type": "filter", + "assignee": { + "id": "5f84d150-7ebd-4c59-997a-df65a5935123" } - ] - ``` + } +] +``` # How-to Guides ## Registering CacheModule Synchronously -To register the CacheModule synchronously, you can use the `register` method. This method allows you to pass configuration options directly. +To register the CacheModule synchronously, you can use the `register` method. +This method allows you to pass configuration options directly. ### Example: ```ts @@ -396,7 +424,9 @@ export class AppModule {} ``` ## Registering CacheModule Asynchronously -For more advanced use cases, you can register the CacheModule asynchronously using the `registerAsync` method. This method is useful when you need to perform asynchronous operations to get the configuration options. +For more advanced use cases, you can register the CacheModule asynchronously using +the `registerAsync` method. This method is useful when you need to perform +asynchronous operations to get the configuration options. ### Example: @@ -423,7 +453,9 @@ export class AppModule {} ``` ## Global Registering CacheModule Asynchronously -For more advanced use cases, you can register the global CacheModule asynchronously using the `forRootAsync` method. This method is useful when you need to perform asynchronous operations to get the configuration options. +For more advanced use cases, you can register the global CacheModule asynchronously +using the `forRootAsync` method. This method is useful when you need to perform +asynchronous operations to get the configuration options. ### Example: ```ts @@ -449,7 +481,8 @@ export class AppModule {} ``` ## Registering CacheModule Asynchronously for multiple entities -This section demonstrates how to register the CacheModule asynchronously when dealing with multiple entities. +This section demonstrates how to register the CacheModule asynchronously when +dealing with multiple entities. ### Example: ```ts @@ -479,16 +512,23 @@ export class AppModule {} ``` ## Using the CacheService to access cache -The `CacheService` provided by the Rockets NestJS Cache module offers a comprehensive set of methods to manage cache entries programmatically from the API side. This service allows for creating, updating, retrieving, and deleting cache entries, as well as managing cache entries for specific assignees. Below is an overview of how to use these services in your application. +The `CacheService` provided by the Rockets NestJS Cache module offers a +comprehensive set of methods to manage cache entries programmatically from the +API side. This service allows for creating, updating, retrieving, and deleting +cache entries, as well as managing cache entries for specific assignees. Below +is an overview of how to use these services in your application. ##### Creating a Cache Entry -To create a new cache entry, you can use the `create` method of the `CacheService`. This method requires specifying the cache assignment, the cache data, and optionally, query options. +To create a new cache entry, you can use the `create` method of the `CacheService`. +This method requires specifying the cache assignment, the cache data, and +optionally, query options. ### CacheService Methods Documentation CacheService is exported in the CacheModule, so -Below is a simple documentation for each method in the `CacheService` class, including examples of how to use them. +Below is a simple documentation for each method in the `CacheService` class, including + examples of how to use them. #### 1. `create(assignment, cache, queryOptions)` Creates a new cache entry. @@ -502,13 +542,13 @@ Creates a new cache entry. Create a cache entry with a unique combination of `key`, `type`, and `assignee.id`: ```ts - await cacheService.create('userCache', { - key: 'userSession', - type: 'session', - data: { sessionData: 'abc123' }, - assignee: { id: 'user1' }, - expiresIn: '24h' - }); +await cacheService.create('userCache', { + key: 'userSession', + type: 'session', + data: { sessionData: 'abc123' }, + assignee: { id: 'user1' }, + expiresIn: '24h' +}); ``` #### 2. `update(assignment, cache, queryOptions)` @@ -596,10 +636,11 @@ Clear all caches for a specific assignee: ```ts await cacheService.clear('userCache', { assignee: { id: 'user1' } }); - ``` -These methods provide a comprehensive interface for managing cache entries in a NestJS application using the `CacheService`. Each method supports optional query options for more granular control over the database operations. +These methods provide a comprehensive interface for managing cache entries in a +NestJS application using the `CacheService`. Each method supports optional query +options for more granular control over the database operations. # Reference @@ -655,12 +696,23 @@ Provides methods for interacting with the cache service. This interface includes ### How CacheOptionsInterface is Used in the Controller and Endpoints -The `CacheSettingsInterface` and `CacheOptionsInterface` are used to configure the caching behavior in the `CacheCrudController`. The `CacheCrudController` provides endpoints for CRUD operations on cache entries and uses these interfaces to manage the settings and services for each cache assignment. +The `CacheSettingsInterface` and `CacheOptionsInterface` are used to configure +the caching behavior in the `CacheCrudController`. The `CacheCrudController` +provides endpoints for CRUD operations on cache entries and uses these +interfaces to manage the settings and services for each cache assignment. -- `CacheSettingsInterface` manages how entities are assigned for caching and specifies the expiration time for cache entries. It is used to ensure the correct service and configuration are applied to each cache assignment. -- `CacheOptionsInterface` includes the settings for managing cache assignments and expiration times. It is used to register and configure the CacheModule, determining which entities should be cached and how they should be handled. +- `CacheSettingsInterface` manages how entities are assigned for caching and + specifies the expiration time for cache entries. It is used to ensure the + correct service and configuration are applied to each cache assignment. +- `CacheOptionsInterface` includes the settings for managing cache assignments + and expiration times. It is used to register and configure the CacheModule, + determining which entities should be cached and how they should be handled. -By using these interfaces, the `CacheCrudController` can dynamically handle different cache assignments and provide a consistent caching strategy across the application. The endpoints in the controller allow for creating, reading, updating, and deleting cache entries, ensuring that the caching behavior is flexible and easily configurable. +By using these interfaces, the `CacheCrudController` can dynamically handle +different cache assignments and provide a consistent caching strategy across +the application. The endpoints in the controller allow for creating, reading, +updating, and deleting cache entries, ensuring that the caching behavior is +flexible and easily configurable. ## Explanation @@ -668,41 +720,78 @@ By using these interfaces, the `CacheCrudController` can dynamically handle diff #### What is Caching? -Caching is a technique used to store copies of data in a temporary storage location (cache) so that future requests for that data can be served faster. It helps in reducing the time required to access data and decreases the load on the primary data source. +Caching is a technique used to store copies of data in a temporary storage location +(cache) so that future requests for that data can be served faster. It helps in +reducing the time required to access data and decreases the load on the primary +data source. #### Benefits of Using Cache -- **Improved Performance**: By serving data from the cache, applications can respond to requests faster than retrieving the data from the primary source each time. -- **Reduced Latency**: Caching reduces the latency involved in data retrieval operations, improving the user experience. -- **Lower Database Load**: By reducing the number of direct database queries, caching helps in decreasing the load on the database, leading to better overall performance. -- **Scalability**: Caching allows applications to handle higher loads by serving frequent requests from the cache instead of the database. +- **Improved Performance**: By serving data from the cache, applications can + respond to requests faster than retrieving the data from the primary source + each time. +- **Reduced Latency**: Caching reduces the latency involved in data retrieval + operations, improving the user experience. +- **Lower Database Load**: By reducing the number of direct database queries, + caching helps in decreasing the load on the database, leading to better overall + performance. +- **Scalability**: Caching allows applications to handle higher loads by serving + frequent requests from the cache instead of the database. #### Why Use NestJS Cache? -NestJS Cache provides a powerful and flexible caching solution that integrates seamlessly with the NestJS framework and stores your cache on the database, so you can reuse it in any other part of your application or even in other applications that are calling your API. It allows developers to manage cached data efficiently and provides built-in support for CRUD operations on cache entries. Here are some key reasons to use NestJS Cache: - -- **Integration with NestJS Ecosystem**: The module integrates well with other NestJS modules and leverages the framework's features, such as decorators and dependency injection. -- **Customizable and Extensible**: It allows for customization through various configuration options and can be extended with custom services and guards. -- **Ease of Use**: The module provides straightforward APIs for managing cache entries, making it easy to implement caching in your application. -- **Automatic Expiration Handling**: The module can automatically handle expiration times for cache entries, ensuring that stale data is not served. +NestJS Cache provides a powerful and flexible caching solution that integrates +seamlessly with the NestJS framework and stores your cache on the database, so +you can reuse it in any other part of your application or even in other +applications that are calling your API. It allows developers to manage cached +data efficiently and provides built-in support for CRUD operations on cache +entries. Here are some key reasons to use NestJS Cache: + +- **Integration with NestJS Ecosystem**: The module integrates well with other + NestJS modules and leverages the framework's features, such as decorators and + dependency injection. +- **Customizable and Extensible**: It allows for customization through various + configuration options and can be extended with custom services and guards. +- **Ease of Use**: The module provides straightforward APIs for managing cache + entries, making it easy to implement caching in your application. +- **Automatic Expiration Handling**: The module can automatically handle + expiration times for cache entries, ensuring that stale data is not served. #### When to Use NestJS Cache -NestJS Cache is useful in scenarios where data is frequently accessed but does not change often. It is also beneficial when the performance of data retrieval operations needs to be improved. Here are some examples of when to use NestJS Cache: +NestJS Cache is useful in scenarios where data is frequently accessed but does +not change often. It is also beneficial when the performance of data retrieval +operations needs to be improved. Here are some examples of when to use NestJS +Cache: -- **Storing Filters for a Specific Dashboard**: If you have a dashboard with complex filters that are expensive to compute, you can cache the filters for each user. This way, subsequent requests can be served from the cache, reducing the load on the server and improving response times. +- **Storing Filters for a Specific Dashboard**: If you have a dashboard with + complex filters that are expensive to compute, you can cache the filters for + each user. This way, subsequent requests can be served from the cache, reducing + the load on the server and improving response times. Example: -When a user applies a filter on a dashboard, the filter settings can be cached. The next time the user accesses the dashboard, the cached filter can be retrieved quickly without recomputing it. +When a user applies a filter on a dashboard, the filter settings can be cached. +The next time the user accesses the dashboard, the cached filter can be retrieved +quickly without recomputing it. #### Design Choices in CacheModule ##### Synchronous vs Asynchronous Registration -- **Synchronous Registration**: This method is used when all configuration options are available at the time of module registration. It is simple and straightforward, making it suitable for most use cases. -- **Asynchronous Registration**: This method is used when configuration options need to be fetched or computed asynchronously. It provides greater flexibility and is useful for advanced scenarios where configuration depends on runtime conditions. +- **Synchronous Registration**: This method is used when all configuration options + are available at the time of module registration. It is simple and + straightforward, making it suitable for most use cases. +- **Asynchronous Registration**: This method is used when configuration options + need to be fetched or computed asynchronously. It provides greater flexibility + and is useful for advanced scenarios where configuration depends on runtime + conditions. ##### Global vs Feature-Specific Registration -- **Global Registration**: Registers the CacheModule at the root level, making it available throughout the entire application. It is useful for shared configurations that need to be applied universally. -- **Feature-Specific Registration**: Registers the CacheModule for specific features or modules, allowing for fine-grained control over caching behavior. It is useful when different parts of the application have unique caching requirements. +- **Global Registration**: Registers the CacheModule at the root level, making it + available throughout the entire application. It is useful for shared + configurations that need to be applied universally. +- **Feature-Specific Registration**: Registers the CacheModule for specific + features or modules, allowing for fine-grained control over caching behavior. + It is useful when different parts of the application have unique caching + requirements. \ No newline at end of file From 8aa52b842dc154ebcde2e1b339d97efad06b1877 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Wed, 12 Jun 2024 15:34:41 -0300 Subject: [PATCH 3/5] chore: improve doc --- packages/nestjs-cache/README.md | 184 ++++++++++++++------------------ 1 file changed, 82 insertions(+), 102 deletions(-) diff --git a/packages/nestjs-cache/README.md b/packages/nestjs-cache/README.md index 0bf0e98e..8416cf6e 100644 --- a/packages/nestjs-cache/README.md +++ b/packages/nestjs-cache/README.md @@ -1,26 +1,57 @@ # Rockets NestJS Cache Documentation +The Rockets NestJS Cache module offers a robust caching solution for NestJS +applications, enhancing data management efficiency. It integrates seamlessly +with the NestJS framework, supporting both synchronous and asynchronous +registration of cache configurations. This module enables CRUD operations on +cache entries directly from the database, facilitating data reuse across +different parts of an application or even different applications. It is +especially useful for boosting application performance, reducing database load, +and improving user experience by minimizing data retrieval times. + +## Project + +[![NPM Latest](https://img.shields.io/npm/v/@concepta/nestjs-auth-local)](https://www.npmjs.com/package/@concepta/nestjs-auth-local) +[![NPM Downloads](https://img.shields.io/npm/dw/@concepta/nestjs-auth-local)](https://www.npmjs.com/package/@concepta/nestjs-auth-local) +[![GH Last Commit](https://img.shields.io/github/last-commit/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets) +[![GH Contrib](https://img.shields.io/github/contributors/conceptadev/rockets?logo=github)](https://github.com/conceptadev/rockets/graphs/contributors) +[![NestJS Dep](https://img.shields.io/github/package-json/dependency-version/conceptadev/rockets/@nestjs/common?label=NestJS&logo=nestjs&filename=packages%2Fnestjs-core%2Fpackage.json)](https://www.npmjs.com/package/@nestjs/common) + ## Table of Contents -- [Rockets NestJS Cache Documentation](#rockets-nestjs-cache-documentation) - - [Table of Contents](#table-of-contents) - - [Tutorials](#tutorials) - - [Getting Started with Rockets NestJS Cache](#getting-started-with-rockets-nestjs-cache) - - [Introduction](#introduction) - - [Installation](#installation) - - [Basic Setup in a NestJS Project](#basic-setup-in-a-nestjs-project) - - [Registering CacheModule Asynchronously for multiple entities](#registering-cachemodule-asynchronously-for-multiple-entities) - - [Reference](#reference) - - [CacheModule API Reference](#cachemodule-api-reference) - - [register(options: CacheOptions)](#registeroptions-cacheoptions) - - [registerAsync(options: CacheAsyncOptions)](#registerasyncoptions-cacheasyncoptions) - - [forRoot(options: CacheOptions)](#forrootoptions-cacheoptions) - - [forRootAsync(options: CacheAsyncOptions)](#forrootasyncoptions-cacheasyncoptions) - - [forFeature(options: CacheOptions)](#forfeatureoptions-cacheoptions) - - [CacheOptionsInterface](#cacheoptionsinterface) - - [CacheModule Classes and Interfaces](#cachemodule-classes-and-interfaces) - - [CacheEntityInterface](#cacheentityinterface) - - [CacheServiceInterface](#cacheserviceinterface) + +- [Tutorials](#tutorials) + - [Getting Started with Rockets NestJS Cache](#getting-started-with-rockets-nestjs-cache) + - [Introduction](#introduction) + - [Installation](#installation) + - [Basic Setup in a NestJS Project](#basic-setup-in-a-nestjs-project) + - [Using the RestFull endpoints to access cache](#using-the-restFull-endpoints-to-access-cache) +- [How-to Guides](#how-to-guides) + - [Registering CacheModule Synchronously](#registering-cachemodule-synchronously) + - [Registering CacheModule Asynchronously](#registering-cachemodule-asynchronously) + - [Global Registering CacheModule Asynchronously](#global-registering-cachemodule-asynchronously) + - [Registering CacheModule Asynchronously for Multiple Entities](#registering-cachemodule-asynchronously-for-multiple-entities) + - [Using the CacheService to Access Cache](#using-the-cacheservice-to-access-cache) +- [Reference](#reference) + - [CacheModule API Reference](#cachemodule-api-reference) + - [register(options: CacheOptions)](#registeroptions-cacheoptions) + - [registerAsync(options: CacheAsyncOptions)](#registerasyncoptions-cacheasyncoptions) + - [forRoot(options: CacheOptions)](#forrootoptions-cacheoptions) + - [forRootAsync(options: CacheAsyncOptions)](#forrootasyncoptions-cacheasyncoptions) + - [forFeature(options: CacheOptions)](#forfeatureoptions-cacheoptions) + - [CacheOptionsInterface](#cacheoptionsinterface) + - [CacheModule Classes and Interfaces](#cachemodule-classes-and-interfaces) + - [CacheEntityInterface](#cacheentityinterface) + - [CacheServiceInterface](#cacheserviceinterface) +- [Explanation](#explanation) + - [Conceptual Overview of Caching](#conceptual-overview-of-caching) + - [What is Caching?](#what-is-caching) + - [Benefits of Using Cache](#benefits-of-using-cache) + - [Why Use NestJS Cache?](#why-use-nestjs-cache) + - [When to Use NestJS Cache](#when-to-use-nestjs-cache) - [How CacheOptionsInterface is Used in the Controller and Endpoints](#how-cacheoptionsinterface-is-used-in-the-controller-and-endpoints) + - [Design Choices in CacheModule](#design-choices-in-cachemodule) + - [Synchronous vs Asynchronous Registration](#synchronous-vs-asynchronous-registration) + - [Global vs Feature-Specific Registration](#global-vs-feature-specific-registration) ## Tutorials @@ -644,77 +675,13 @@ options for more granular control over the database operations. # Reference -Detailed Descriptions of All Classes, Methods, and Properties - -### CacheModule API Reference - -#### register(options: CacheOptions) -Registers the module with synchronous options. - -#### registerAsync(options: CacheAsyncOptions) -Registers the module with asynchronous options. - -#### forRoot(options: CacheOptions) -Registers the module globally with synchronous options. - -#### forRootAsync(options: CacheAsyncOptions) -Registers the module globally with asynchronous options. - -#### forFeature(options: CacheOptions) -Registers the module for specific features with custom options. +For detailed information on the properties, methods, and classes used in the +`@concepta/nestjs-cache`, please refer to the API documentation +available at [CacheModule API Documentation](). This documentation provides +comprehensive details on the interfaces and services that you can utilize to +start using cache functionality within your NestJS application. -### CacheOptionsInterface - -The CacheOptionsInterface provides various configuration options to customize the behavior of the CacheModule. Below is a summary of the key options: - -#### settings (required) -Manages how entities are assigned for caching. It includes properties like: -- `assignments`: An object mapping assignments to entity keys. the name defined here will be the route for client to access and it should be used on entities as well to match with the entity class -- `expiresIn`: Optional property defining the default expiration time for cache entries. - -#### entities (required) -Specifies which entities are to be cached. It includes properties like: -- `entities`: A record of entities and their corresponding configuration options. - -### CacheModule Classes and Interfaces - -#### CacheEntityInterface -Defines the structure of a cache entity. This interface includes properties such as: -- `id`: Unique identifier for the cache entry. -- `key`: Key used to reference the cache entry. -- `type`: Type of data being cached. -- `data`: The actual data being cached. -- `assignee`: The entity associated with the cache entry. -- `expiresIn`: The expiration time for the cache entry. - -#### CacheServiceInterface -Provides methods for interacting with the cache service. This interface includes methods such as: -- `create`: Method to create a new cache entry. -- `findOne`: Method to find a cache entry by its ID. -- `update`: Method to update an existing cache entry. -- `remove`: Method to remove a cache entry. - -### How CacheOptionsInterface is Used in the Controller and Endpoints - -The `CacheSettingsInterface` and `CacheOptionsInterface` are used to configure -the caching behavior in the `CacheCrudController`. The `CacheCrudController` -provides endpoints for CRUD operations on cache entries and uses these -interfaces to manage the settings and services for each cache assignment. - -- `CacheSettingsInterface` manages how entities are assigned for caching and - specifies the expiration time for cache entries. It is used to ensure the - correct service and configuration are applied to each cache assignment. -- `CacheOptionsInterface` includes the settings for managing cache assignments - and expiration times. It is used to register and configure the CacheModule, - determining which entities should be cached and how they should be handled. - -By using these interfaces, the `CacheCrudController` can dynamically handle -different cache assignments and provide a consistent caching strategy across -the application. The endpoints in the controller allow for creating, reading, -updating, and deleting cache entries, ensuring that the caching behavior is -flexible and easily configurable. - -## Explanation +# Explanation ### Conceptual Overview of Caching @@ -738,7 +705,7 @@ data source. - **Scalability**: Caching allows applications to handle higher loads by serving frequent requests from the cache instead of the database. -#### Why Use NestJS Cache? +#### Why Use NestJS Rockets Cache? NestJS Cache provides a powerful and flexible caching solution that integrates seamlessly with the NestJS framework and stores your cache on the database, so @@ -774,24 +741,37 @@ When a user applies a filter on a dashboard, the filter settings can be cached. The next time the user accesses the dashboard, the cached filter can be retrieved quickly without recomputing it. +#### How CacheOptionsInterface is Used in the Controller and Endpoints + +The `CacheSettingsInterface` and `CacheOptionsInterface` are used to configure +the caching behavior in the `CacheCrudController`. The `CacheCrudController` +provides endpoints for CRUD operations on cache entries and uses these +interfaces to manage the settings and services for each cache assignment. + +- `CacheSettingsInterface` manages how entities are assigned for caching and + specifies the expiration time for cache entries. It is used to ensure the + correct service and configuration are applied to each cache assignment. +- `CacheOptionsInterface` includes the settings for managing cache assignments + and expiration times. It is used to register and configure the CacheModule, + determining which entities should be cached and how they should be handled. + +By using these interfaces, the `CacheCrudController` can dynamically handle +different cache assignments and provide a consistent caching strategy across +the application. The endpoints in the controller allow for creating, reading, +updating, and deleting cache entries, ensuring that the caching behavior is +flexible and easily configurable. + #### Design Choices in CacheModule -##### Synchronous vs Asynchronous Registration +##### Global vs Synchronous vs Asynchronous Registration +- **Global Registration**: Registers the CacheModule at the root level, making it + available throughout the entire application. It is useful for shared + configurations that need to be applied universally. - **Synchronous Registration**: This method is used when all configuration options are available at the time of module registration. It is simple and straightforward, making it suitable for most use cases. - **Asynchronous Registration**: This method is used when configuration options need to be fetched or computed asynchronously. It provides greater flexibility and is useful for advanced scenarios where configuration depends on runtime - conditions. - -##### Global vs Feature-Specific Registration - -- **Global Registration**: Registers the CacheModule at the root level, making it - available throughout the entire application. It is useful for shared - configurations that need to be applied universally. -- **Feature-Specific Registration**: Registers the CacheModule for specific - features or modules, allowing for fine-grained control over caching behavior. - It is useful when different parts of the application have unique caching - requirements. \ No newline at end of file + conditions. \ No newline at end of file From 78e7168067327774565c82ad8c86e93cbbc5c639 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Thu, 13 Jun 2024 15:53:37 -0300 Subject: [PATCH 4/5] feat: add error improvement for duplicated cache --- packages/nestjs-cache/README.md | 6 ++- packages/nestjs-cache/package.json | 1 + .../src/__fixtures__/app.module.fixture.ts | 9 +++- .../cache-crud.controller.e2e-spec.ts | 8 +++- .../src/controllers/cache-crud.controller.ts | 43 ++++++++++++++----- ...he-entity-already-exists.exception.spec.ts | 25 +++++++++++ .../cache-entity-already-exists.exception.ts | 26 +++++++++++ .../src/services/cache.service.spec.ts | 13 +++++- .../src/services/cache.service.ts | 29 +++++++++---- 9 files changed, 137 insertions(+), 23 deletions(-) create mode 100644 packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts create mode 100644 packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.ts diff --git a/packages/nestjs-cache/README.md b/packages/nestjs-cache/README.md index 8416cf6e..cc98f5d6 100644 --- a/packages/nestjs-cache/README.md +++ b/packages/nestjs-cache/README.md @@ -194,7 +194,7 @@ export class UserModule {} needed. Please note that `CacheSqliteEntity` and `CachePostgresEntity` are provided by the Rockets NestJS Cache module, so you can use them to create your cache entity. They have a unique index with the following properties: - `'key', 'type', 'assignee.id'`: + `'key', 'type', 'assignee.id'` and it will throw a `CacheEntityAlreadyExistsException` if duplicated: ```typescript import { Entity, ManyToOne } from 'typeorm'; @@ -291,7 +291,9 @@ entities: { This will make the following endpoints available: 1. **Create (POST)**: To create a new cache entry, the request body should -match the `CacheCreatableInterface`: +match the `CacheCreatableInterface`; Properties `key, type and assignee.id` +are unique and will throw a `CacheEntityAlreadyExistsException` error on +attempt to insert duplicated data: ```ts export interface CacheCreatableInterface extends Pick { diff --git a/packages/nestjs-cache/package.json b/packages/nestjs-cache/package.json index 97c32fe8..0a70d767 100644 --- a/packages/nestjs-cache/package.json +++ b/packages/nestjs-cache/package.json @@ -21,6 +21,7 @@ "@concepta/ts-common": "^4.0.0-alpha.46", "@concepta/ts-core": "^4.0.0-alpha.46", "@concepta/typeorm-common": "^4.0.0-alpha.46", + "@nestjs/core": "^9.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/swagger": "^6.0.0", diff --git a/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts index f6f58fd6..0bd8b88e 100644 --- a/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts +++ b/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; import { CrudModule } from '@concepta/nestjs-crud'; - +import { APP_FILTER } from '@nestjs/core'; import { CacheModule } from '../cache.module'; import { UserEntityFixture } from './entities/user-entity.fixture'; import { UserCacheEntityFixture } from './entities/user-cache-entity.fixture'; +import { ExceptionsFilter } from '@concepta/nestjs-exception'; @Module({ imports: [ @@ -28,5 +29,11 @@ import { UserCacheEntityFixture } from './entities/user-cache-entity.fixture'; }), CrudModule.forRoot({}), ], + providers: [ + { + provide: APP_FILTER, + useClass: ExceptionsFilter, + }, + ], }) export class AppModuleFixture {} diff --git a/packages/nestjs-cache/src/controllers/cache-crud.controller.e2e-spec.ts b/packages/nestjs-cache/src/controllers/cache-crud.controller.e2e-spec.ts index bbe3e6b8..517539f8 100644 --- a/packages/nestjs-cache/src/controllers/cache-crud.controller.e2e-spec.ts +++ b/packages/nestjs-cache/src/controllers/cache-crud.controller.e2e-spec.ts @@ -151,7 +151,13 @@ describe('CacheAssignmentController (e2e)', () => { await supertest(app.getHttpServer()) .post('/cache/user') .send(payload) - .expect(500); + .then((res) => { + // check error message + expect(res.body.message).toBe( + 'userCache already exists with the given key, type, and assignee ID.', + ); + expect(res.status).toBe(400); + }); }); it('DELETE /cache/user/:id', async () => { diff --git a/packages/nestjs-cache/src/controllers/cache-crud.controller.ts b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts index 7e66296f..7846f905 100644 --- a/packages/nestjs-cache/src/controllers/cache-crud.controller.ts +++ b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts @@ -37,7 +37,8 @@ import { CacheEntityNotFoundException } from '../exceptions/cache-entity-not-fou import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; import { CacheCrudService } from '../services/cache-crud.service'; import getExpirationDate from '../utils/get-expiration-date.util'; - +import { CacheService } from '../services/cache.service'; +import { CacheEntityAlreadyExistsException } from '../exceptions/cache-entity-already-exists.exception'; /** * Cache assignment controller. */ @@ -76,6 +77,7 @@ export class CacheCrudController private settings: CacheSettingsInterface, @Inject(CACHE_MODULE_CRUD_SERVICES_TOKEN) private allCrudServices: Record, + private cacheService: CacheService, ) {} /** @@ -126,6 +128,17 @@ export class CacheCrudController cacheCreateDto.expiresIn ?? this.settings.expiresIn, ); + const existingCache = await this.cacheService.get( + assignment, + cacheCreateDto, + ); + + if (existingCache) { + throw new CacheEntityAlreadyExistsException( + this.getEntityKey(assignment), + ); + } + // call crud service to create return this.getCrudService(assignment).createOne(crudRequest, { ...cacheCreateDto, @@ -180,21 +193,31 @@ export class CacheCrudController * @param assignment The cache assignment */ protected getCrudService(assignment: ReferenceAssignment): CacheCrudService { + const entityKey = this.getEntityKey(assignment); + // repo matching assignment was injected? + if (this.allCrudServices[entityKey]) { + // yes, return it + return this.allCrudServices[entityKey]; + } else { + // bad entity key + throw new CacheEntityNotFoundException(entityKey); + } + } + + /** + * Get the entity key for the given assignment. + * + * @param assignment The cache assignment + */ + protected getEntityKey(assignment: ReferenceAssignment): string { // have entity key for given assignment? if (this.settings.assignments[assignment]) { // yes, set it - const entityKey = this.settings.assignments[assignment].entityKey; - // repo matching assignment was injected? - if (this.allCrudServices[entityKey]) { - // yes, return it - return this.allCrudServices[entityKey]; - } else { - // bad entity key - throw new CacheEntityNotFoundException(entityKey); - } + return this.settings.assignments[assignment].entityKey; } else { // bad assignment throw new CacheAssignmentNotFoundException(assignment); } } } + diff --git a/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts new file mode 100644 index 00000000..395be685 --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts @@ -0,0 +1,25 @@ +import { CacheEntityAlreadyExistsException } from './cache-entity-already-exists.exception'; + +describe(CacheEntityAlreadyExistsException.name, () => { + it('should create an instance of CacheEntityAlreadyExistsException', () => { + const exception = new CacheEntityAlreadyExistsException('TestEntity'); + expect(exception).toBeInstanceOf(CacheEntityAlreadyExistsException); + }); + + it('should have the correct error message', () => { + const exception = new CacheEntityAlreadyExistsException('TestEntity'); + expect(exception.message).toBe( + 'TestEntity already exists with the given key, type, and assignee ID.', + ); + }); + + it('should have the correct context', () => { + const exception = new CacheEntityAlreadyExistsException('TestEntity'); + expect(exception.context).toEqual({ entityName: 'TestEntity' }); + }); + + it('should have the correct error code', () => { + const exception = new CacheEntityAlreadyExistsException('TestEntity'); + expect(exception.errorCode).toBe('CACHE_ENTITY_ALREADY_EXISTS_ERROR'); + }); +}); \ No newline at end of file diff --git a/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.ts b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.ts new file mode 100644 index 00000000..b7cd17e6 --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.ts @@ -0,0 +1,26 @@ +import { RuntimeException } from '@concepta/nestjs-exception'; +import { HttpStatus } from '@nestjs/common'; + +export class CacheEntityAlreadyExistsException extends RuntimeException { + context: RuntimeException['context'] & { + entityName: string; + }; + + constructor( + entityName: string, + message = '%s already exists with the given key, type, and assignee ID.', + ) { + super({ + httpStatus: HttpStatus.BAD_REQUEST, + message, + messageParams: [entityName], + }); + + this.errorCode = 'CACHE_ENTITY_ALREADY_EXISTS_ERROR'; + + this.context = { + ...super.context, + entityName, + }; + } +} diff --git a/packages/nestjs-cache/src/services/cache.service.spec.ts b/packages/nestjs-cache/src/services/cache.service.spec.ts index f29d3e35..79f887b1 100644 --- a/packages/nestjs-cache/src/services/cache.service.spec.ts +++ b/packages/nestjs-cache/src/services/cache.service.spec.ts @@ -41,6 +41,9 @@ describe('CacheService', () => { beforeEach(() => { repo = mock>(); settings = mock(); + settings.assignments = { + testAssignment: { entityKey: 'testAssignment' }, + }; settings.expiresIn = '1h'; service = new CacheService({ testAssignment: repo }, settings); }); @@ -101,7 +104,6 @@ describe('CacheService', () => { repoProxyMock.repository.mockReturnValue(repo); service['validateDto'] = jest.fn().mockResolvedValueOnce(cacheDto); - service['findCache'] = jest.fn(); const result = { key: cacheDto.key, type: cacheDto.type, @@ -109,6 +111,15 @@ describe('CacheService', () => { assignee: cacheDto.assignee, expirationDate, }; + service['findCache'] = jest.fn().mockImplementationOnce(() => { + return { + ...result, + dateCreated: new Date(), + dateUpdated: new Date(), + id: 'testId', + version: 1, + } as CacheInterface; + }); service['mergeEntity'] = jest.fn().mockResolvedValue(result); jest.spyOn(RepositoryProxy.prototype, 'repository').mockReturnValue(repo); diff --git a/packages/nestjs-cache/src/services/cache.service.ts b/packages/nestjs-cache/src/services/cache.service.ts index 35284b7e..658977e6 100644 --- a/packages/nestjs-cache/src/services/cache.service.ts +++ b/packages/nestjs-cache/src/services/cache.service.ts @@ -25,6 +25,7 @@ import { CacheEntityNotFoundException } from '../exceptions/cache-entity-not-fou import { CacheServiceInterface } from '../interfaces/cache-service.interface'; import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; import getExpirationDate from '../utils/get-expiration-date.util'; +import { CacheAssignmentNotFoundException } from '../exceptions/cache-assignment-not-found.exception'; @Injectable() export class CacheService implements CacheServiceInterface { @@ -99,6 +100,10 @@ export class CacheService implements CacheServiceInterface { // try to update the item try { const assignedCache = await this.findCache(repoProxy, dto, queryOptions); + if (!assignedCache) + throw new CacheEntityNotFoundException( + assignmentRepo.metadata.targetName, + ); const mergedEntity = await this.mergeEntity( repoProxy, @@ -265,10 +270,11 @@ export class CacheService implements CacheServiceInterface { repoProxy: RepositoryProxy, cache: Pick, queryOptions?: QueryOptionsInterface, - ): Promise { + ): Promise { const { key, type, assignee } = cache; try { - const cache = await repoProxy.repository(queryOptions).findOne({ + const repo = repoProxy.repository(queryOptions); + const cache = await repo.findOne({ where: { key, type, @@ -276,7 +282,6 @@ export class CacheService implements CacheServiceInterface { }, relations: ['assignee'], }); - if (!cache) throw new Error('Could not find repository'); return cache; } catch (e) { throw new ReferenceLookupException( @@ -295,13 +300,21 @@ export class CacheService implements CacheServiceInterface { protected getAssignmentRepo( assignment: ReferenceAssignment, ): Repository { - // repo matching assignment was injected? - if (this.allCacheRepos[assignment]) { - // yes, return it - return this.allCacheRepos[assignment]; + if (this.settings.assignments[assignment]) { + // get entity key based on assignment + const entityKey = this.settings.assignments[assignment].entityKey; + + // repo matching assignment was injected? + if (this.allCacheRepos[entityKey]) { + // yes, return it + return this.allCacheRepos[entityKey]; + } else { + // bad assignment + throw new CacheEntityNotFoundException(entityKey); + } } else { // bad assignment - throw new CacheEntityNotFoundException(assignment); + throw new CacheAssignmentNotFoundException(assignment); } } From 4fbaa87373efdcde0665368190c157b686c3901a Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Mon, 17 Jun 2024 17:32:36 -0300 Subject: [PATCH 5/5] chore: fix lint --- packages/nestjs-cache/src/controllers/cache-crud.controller.ts | 1 - .../exceptions/cache-entity-already-exists.exception.spec.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nestjs-cache/src/controllers/cache-crud.controller.ts b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts index 7846f905..335832c7 100644 --- a/packages/nestjs-cache/src/controllers/cache-crud.controller.ts +++ b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts @@ -220,4 +220,3 @@ export class CacheCrudController } } } - diff --git a/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts index 395be685..89abd362 100644 --- a/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts +++ b/packages/nestjs-cache/src/exceptions/cache-entity-already-exists.exception.spec.ts @@ -22,4 +22,4 @@ describe(CacheEntityAlreadyExistsException.name, () => { const exception = new CacheEntityAlreadyExistsException('TestEntity'); expect(exception.errorCode).toBe('CACHE_ENTITY_ALREADY_EXISTS_ERROR'); }); -}); \ No newline at end of file +});