diff --git a/packages/nestjs-cache/README.md b/packages/nestjs-cache/README.md new file mode 100644 index 000000000..6ceef7b91 --- /dev/null +++ b/packages/nestjs-cache/README.md @@ -0,0 +1,227 @@ +# 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) + +## Table of Contents + +- [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. + +## Usage + +To utilize the caching module, you need to define the entities and configure the module appropriately. + +## Example +Define your UserEntityFixture and UserCacheEntityFixture entities as follows: + +```ts +@Entity() +export class UserEntityFixture implements ReferenceIdInterface { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ default: false }) + isActive!: boolean; + + @OneToMany(() => UserCacheEntityFixture, (userCache) => userCache.assignee) + userCaches!: UserCacheEntityFixture[]; +} + +``` + +```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. + +### Configure the Cache Module +Incorporate the Cache Module into your application module and configure it for your specific needs: + +```ts +// ... + +@Module({ + imports: [ + TypeOrmExtModule.forRoot({ + type: 'postgres', + url: 'postgres://user:pass@localhost:5432/postgres', + }), + CacheModule.register({ + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + }, + }), + CacheModule.forRoot({}), + ], +}) +export class AppModule {} +``` + +### Client-side Interaction with CRUD Endpoints + +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: + +```typescript +CacheModule.register({ + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + }, +}) +``` +The endpoint will be cache/user. Below are examples of how to interact with the API using curl. + +### Create (POST) +To create a new cache entry, the request body should match the CacheCreatableInterface: + +```ts +export interface CacheCreatableInterface extends Pick { + expiresIn: string | null; +} +``` +Example curl command: + +```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" +}' + +``` +### Read (GET) +To read a cache entry by its ID: + +```sh +curl -X GET http://your-api-url/cache/user/{id} +``` + +### Update (PUT) +To update an existing cache entry, the request body should match the CacheUpdatableInterface: + +```ts +export interface CacheUpdatableInterface extends Pick { + expiresIn: string | null; +} + +``` +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" +}' + +``` +### Delete (DELETE) +To delete a cache entry by its 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. + +By following these examples, you can perform Create, Read, Update, and Delete operations on your cache data using the provided endpoints. + +## How the Module Works + +### Overview + +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: + +### Dynamic Controller Generation + +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. + +### CRUD Operations + +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. + +### Service Injection + +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. + +### Handling Assignments + +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. + +### Exception Handling + +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. + +### Summary + +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. + + +#### ENV + +Configurations available via environment. + +| 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 | diff --git a/packages/nestjs-cache/package.json b/packages/nestjs-cache/package.json new file mode 100644 index 000000000..f045abbbb --- /dev/null +++ b/packages/nestjs-cache/package.json @@ -0,0 +1,43 @@ +{ + "name": "@concepta/nestjs-cache", + "version": "4.0.0-alpha.42", + "description": "Rockets NestJS User", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/**/!(*.spec|*.e2e-spec|*.fixture).{js,d.ts}" + ], + "dependencies": { + "@concepta/nestjs-access-control": "^4.0.0-alpha.42", + "@concepta/nestjs-common": "^4.0.0-alpha.42", + "@concepta/nestjs-core": "^4.0.0-alpha.42", + "@concepta/nestjs-crud": "^4.0.0-alpha.42", + "@concepta/nestjs-typeorm-ext": "^4.0.0-alpha.42", + "@concepta/ts-common": "^4.0.0-alpha.42", + "@concepta/ts-core": "^4.0.0-alpha.42", + "@concepta/typeorm-common": "^4.0.0-alpha.42", + "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.2.0", + "@nestjs/swagger": "^6.0.0", + "ms": "^2.1.3" + }, + "devDependencies": { + "@concepta/typeorm-seeding": "^4.0.0-beta.0", + "@faker-js/faker": "^6.0.0-alpha.6", + "@nestjs/testing": "^9.0.0", + "@nestjs/typeorm": "^9.0.0", + "@types/ms": "^0.7.31", + "@types/supertest": "^2.0.11", + "jest-mock-extended": "^2.0.4", + "supertest": "^6.1.3" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "typeorm": "^0.3.0" + } +} diff --git a/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts new file mode 100644 index 000000000..39581e5c8 --- /dev/null +++ b/packages/nestjs-cache/src/__fixtures__/app.module.fixture.ts @@ -0,0 +1,33 @@ +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { Module } from '@nestjs/common'; + +import { CacheModule } from '../cache.module'; +import { UserEntityFixture } from './entities/user-entity.fixture'; +import { UserCacheEntityFixture } from './entities/user-cache-entity.fixture'; +import { CrudModule } from '@concepta/nestjs-crud'; + +@Module({ + imports: [ + TypeOrmExtModule.forRoot({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + entities: [UserEntityFixture, UserCacheEntityFixture], + }), + CacheModule.register({ + entities: { + userCache: { + entity: UserCacheEntityFixture, + }, + }, + settings: { + assignments: { + user: { entityKey: 'userCache' }, + }, + }, + }), + CrudModule.forRoot({}), + ], +}) +export class AppModuleFixture { } + diff --git a/packages/nestjs-cache/src/__fixtures__/entities/user-cache-entity.fixture.ts b/packages/nestjs-cache/src/__fixtures__/entities/user-cache-entity.fixture.ts new file mode 100644 index 000000000..d1c0ccee4 --- /dev/null +++ b/packages/nestjs-cache/src/__fixtures__/entities/user-cache-entity.fixture.ts @@ -0,0 +1,13 @@ +import { Entity, ManyToOne } from 'typeorm'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { UserEntityFixture } from './user-entity.fixture'; +import { CacheSqliteEntity } from '../../entities/cache-sqlite.entity'; + +/** + * Cache Entity Fixture + */ +@Entity() +export class UserCacheEntityFixture extends CacheSqliteEntity { + @ManyToOne(() => UserEntityFixture, (user) => user.userCaches) + assignee!: ReferenceIdInterface; +} diff --git a/packages/nestjs-cache/src/__fixtures__/entities/user-entity.fixture.ts b/packages/nestjs-cache/src/__fixtures__/entities/user-entity.fixture.ts new file mode 100644 index 000000000..3009c3b0e --- /dev/null +++ b/packages/nestjs-cache/src/__fixtures__/entities/user-entity.fixture.ts @@ -0,0 +1,18 @@ +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { UserCacheEntityFixture } from './user-cache-entity.fixture'; + +/** + * User Entity Fixture + */ +@Entity() +export class UserEntityFixture implements ReferenceIdInterface { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ default: false }) + isActive!: boolean; + + @OneToMany(() => UserCacheEntityFixture, (userCache) => userCache.assignee) + userCaches!: UserCacheEntityFixture[]; +} diff --git a/packages/nestjs-cache/src/__fixtures__/factories/user-cache.factory.fixture.ts b/packages/nestjs-cache/src/__fixtures__/factories/user-cache.factory.fixture.ts new file mode 100644 index 000000000..1bcda1575 --- /dev/null +++ b/packages/nestjs-cache/src/__fixtures__/factories/user-cache.factory.fixture.ts @@ -0,0 +1,21 @@ + +import { Factory } from '@concepta/typeorm-seeding'; +import { UserCacheEntityFixture } from '../entities/user-cache-entity.fixture'; +import Faker from '@faker-js/faker'; + +export class UserCacheFactoryFixture extends Factory { + protected options = { + entity: UserCacheEntityFixture, + }; + + protected async entity( + userCache: UserCacheEntityFixture, + ): Promise { + userCache.key = Faker.name.jobArea(); + userCache.type = 'filter'; + userCache.data = JSON.stringify({ sortBy: 'name' }); + userCache.expirationDate = new Date(); + + return userCache; + } +} diff --git a/packages/nestjs-cache/src/__fixtures__/factories/user.factory.fixture.ts b/packages/nestjs-cache/src/__fixtures__/factories/user.factory.fixture.ts new file mode 100644 index 000000000..f1d79744a --- /dev/null +++ b/packages/nestjs-cache/src/__fixtures__/factories/user.factory.fixture.ts @@ -0,0 +1,8 @@ +import { Factory } from '@concepta/typeorm-seeding'; +import { UserEntityFixture } from '../entities/user-entity.fixture'; + +export class UserFactoryFixture extends Factory { + options = { + entity: UserEntityFixture, + }; +} diff --git a/packages/nestjs-cache/src/cache.constants.ts b/packages/nestjs-cache/src/cache.constants.ts new file mode 100644 index 000000000..618288495 --- /dev/null +++ b/packages/nestjs-cache/src/cache.constants.ts @@ -0,0 +1,8 @@ +export const CACHE_MODULE_SETTINGS_TOKEN = 'CACHE_MODULE_SETTINGS_TOKEN'; +export const CACHE_MODULE_REPOSITORIES_TOKEN = + 'CACHE_MODULE_REPOSITORIES_TOKEN'; +export const CACHE_MODULE_CRUD_SERVICES_TOKEN = + 'CACHE_MODULE_CRUD_SERVICES_TOKEN'; +export const CACHE_MODULE_DEFAULT_SETTINGS_TOKEN = + 'CACHE_MODULE_DEFAULT_SETTINGS_TOKEN'; +export const CACHE_MODULE_CACHE_ENTITY_KEY = 'cache'; diff --git a/packages/nestjs-cache/src/cache.factory.ts b/packages/nestjs-cache/src/cache.factory.ts new file mode 100644 index 000000000..bc929b55b --- /dev/null +++ b/packages/nestjs-cache/src/cache.factory.ts @@ -0,0 +1,43 @@ +import { randomUUID } from 'crypto'; +import Faker from '@faker-js/faker'; +import { Factory } from '@concepta/typeorm-seeding'; +import { CacheInterface } from '@concepta/ts-common'; +/** + * Cache factory + */ +export class CacheFactory extends Factory { + /** + * List of used names. + */ + keys: string[] = ['filter', 'sort', 'list']; + + /** + * Factory callback function. + */ + protected async entity(cache: CacheInterface): Promise { + const fakeFilter = { + name: Faker.name.firstName, + orderBy: 'name', + }; + + // set the name + cache.key = randomUUID(); + cache.type = this.randomKey(); + cache.data = JSON.stringify(fakeFilter); + cache.expirationDate = new Date(); + + // return the new cache + return cache; + } + + /** + * Get a random category. + */ + protected randomKey(): string { + // random index + const randomIdx = Math.floor(Math.random() * this.keys.length); + + // return it + return this.keys[randomIdx]; + } +} diff --git a/packages/nestjs-cache/src/cache.module-definition.ts b/packages/nestjs-cache/src/cache.module-definition.ts new file mode 100644 index 000000000..8938b158d --- /dev/null +++ b/packages/nestjs-cache/src/cache.module-definition.ts @@ -0,0 +1,171 @@ +import { + ConfigurableModuleBuilder, + DynamicModule, + Provider, +} from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { createSettingsProvider } from '@concepta/nestjs-common'; +import { + getDynamicRepositoryToken, + TypeOrmExtModule, +} from '@concepta/nestjs-typeorm-ext'; + +import { + CACHE_MODULE_CRUD_SERVICES_TOKEN, + CACHE_MODULE_REPOSITORIES_TOKEN, + CACHE_MODULE_SETTINGS_TOKEN, +} from './cache.constants'; + +import { CacheEntitiesOptionsInterface } from './interfaces/cache-entities-options.interface'; +import { CacheOptionsExtrasInterface } from './interfaces/cache-options-extras.interface'; +import { CacheOptionsInterface } from './interfaces/cache-options.interface'; +import { CacheSettingsInterface } from './interfaces/cache-settings.interface'; + +import { CacheInterface } from '@concepta/ts-common'; +import { Repository } from 'typeorm'; +import { cacheDefaultConfig } from './config/cache-default.config'; +import { CacheCrudController } from './controllers/cache-crud.controller'; +import { CacheCrudService } from './services/cache-crud.service'; +import { CacheService } from './services/cache.service'; + +const RAW_OPTIONS_TOKEN = Symbol('__CACHE_MODULE_RAW_OPTIONS_TOKEN__'); + +export const { + ConfigurableModuleClass: CacheModuleClass, + OPTIONS_TYPE: CACHE_OPTIONS_TYPE, + ASYNC_OPTIONS_TYPE: CACHE_ASYNC_OPTIONS_TYPE, +} = new ConfigurableModuleBuilder({ + moduleName: 'Cache', + optionsInjectionToken: RAW_OPTIONS_TOKEN, +}) + .setExtras( + { global: false }, + definitionTransform, + ) + .build(); + +export type CacheOptions = Omit; +export type CacheAsyncOptions = Omit; + +function definitionTransform( + definition: DynamicModule, + extras: CacheOptionsExtrasInterface, +): DynamicModule { + const { providers } = definition; + const { controllers, global = false, entities } = extras; + + if (!entities) { + throw new Error('You must provide the entities option'); + } + + return { + ...definition, + global, + imports: createCacheImports({ entities }), + providers: createCacheProviders({ entities, providers }), + exports: [ConfigModule, RAW_OPTIONS_TOKEN, ...createCacheExports()], + controllers: createCacheControllers({ controllers }), + }; +} + +export function createCacheControllers( + overrides: Pick = {}, +): DynamicModule['controllers'] { + return overrides?.controllers !== undefined + ? overrides.controllers + : [CacheCrudController]; +} + +export function createCacheImports( + options: CacheEntitiesOptionsInterface, +): DynamicModule['imports'] { + return [ + ConfigModule.forFeature(cacheDefaultConfig), + TypeOrmExtModule.forFeature(options.entities), + ]; +} + +export function createCacheProviders( + options: CacheEntitiesOptionsInterface & { + overrides?: CacheOptions; + providers?: Provider[]; + }, +): Provider[] { + return [ + ...(options.providers ?? []), + createCacheSettingsProvider(options.overrides), + ...createCacheRepositoriesProvider({ + entities: options.overrides?.entities ?? options.entities, + }), + CacheService, + ]; +} + +export function createCacheExports(): Required< + Pick +>['exports'] { + return [ + CACHE_MODULE_SETTINGS_TOKEN, + CACHE_MODULE_REPOSITORIES_TOKEN, + CacheService, + ]; +} + +export function createCacheSettingsProvider( + optionsOverrides?: CacheOptions, +): Provider { + return createSettingsProvider({ + settingsToken: CACHE_MODULE_SETTINGS_TOKEN, + optionsToken: RAW_OPTIONS_TOKEN, + settingsKey: cacheDefaultConfig.KEY, + optionsOverrides, + }); +} + +export function createCacheRepositoriesProvider( + options: CacheEntitiesOptionsInterface, +): Provider[] { + const { entities } = options; + + const reposToInject = []; + const keyTracker: Record = {}; + + let entityIdx = 0; + + for (const entityKey in entities) { + reposToInject[entityIdx] = getDynamicRepositoryToken(entityKey); + keyTracker[entityKey] = entityIdx++; + } + + return [ + { + provide: CACHE_MODULE_REPOSITORIES_TOKEN, + inject: reposToInject, + useFactory: (...args: string[]) => { + const repoInstances: Record = {}; + + for (const entityKey in entities) { + repoInstances[entityKey] = args[keyTracker[entityKey]]; + } + + return repoInstances; + }, + }, + { + provide: CACHE_MODULE_CRUD_SERVICES_TOKEN, + inject: reposToInject, + useFactory: (...args: Repository[]) => { + const serviceInstances: Record = {}; + + for (const entityKey in entities) { + serviceInstances[entityKey] = new CacheCrudService( + args[keyTracker[entityKey]], + ); + } + + return serviceInstances; + }, + }, + ]; +} diff --git a/packages/nestjs-cache/src/cache.module.spec.ts b/packages/nestjs-cache/src/cache.module.spec.ts new file mode 100644 index 000000000..9ad0fa6c5 --- /dev/null +++ b/packages/nestjs-cache/src/cache.module.spec.ts @@ -0,0 +1,69 @@ +import { Repository } from 'typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CacheInterface } from '@concepta/ts-common'; +import { CACHE_MODULE_REPOSITORIES_TOKEN } from './cache.constants'; +import { CacheModule } from './cache.module'; + +import { AppModuleFixture } from './__fixtures__/app.module.fixture'; +import { DynamicModule } from '@nestjs/common'; +import { CacheService } from './services/cache.service'; + +describe(CacheModule.name, () => { + let cacheModule: CacheModule; + let cacheService: CacheService; + let cacheDynamicRepo: Record>; + + beforeEach(async () => { + const testModule: TestingModule = await Test.createTestingModule({ + imports: [AppModuleFixture], + }).compile(); + + cacheModule = testModule.get(CacheModule); + cacheService = testModule.get(CacheService); + cacheDynamicRepo = testModule.get< + Record> + >(CACHE_MODULE_REPOSITORIES_TOKEN); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('module', () => { + it('should be loaded', async () => { + expect(cacheModule).toBeInstanceOf(CacheModule); + expect(cacheService).toBeInstanceOf(CacheService); + expect(cacheDynamicRepo).toBeDefined(); + }); + }); + + describe('CacheModule functions', () => { + const spyRegister = jest + .spyOn(CacheModule, 'register') + .mockImplementation(() => { + return {} as DynamicModule; + }); + + const spyRegisterAsync = jest + .spyOn(CacheModule, 'registerAsync') + .mockImplementation(() => { + return {} as DynamicModule; + }); + + it('should call super.register in register method', () => { + CacheModule.register({}); + expect(spyRegister).toHaveBeenCalled(); + }); + + it('should call super.registerAsync in register method', () => { + CacheModule.registerAsync({}); + expect(spyRegisterAsync).toHaveBeenCalled(); + }); + + it('should throw an error in forFeature method', () => { + expect(() => CacheModule.forFeature({})).toThrow( + 'You must provide the entities option', + ); + }); + }); +}); diff --git a/packages/nestjs-cache/src/cache.module.ts b/packages/nestjs-cache/src/cache.module.ts new file mode 100644 index 000000000..e566fde4e --- /dev/null +++ b/packages/nestjs-cache/src/cache.module.ts @@ -0,0 +1,47 @@ +import { DynamicModule, Module } from '@nestjs/common'; + +import { + CacheAsyncOptions, + CacheModuleClass, + CacheOptions, + createCacheExports, + createCacheImports, + createCacheProviders, +} from './cache.module-definition'; + +/** + * Cache Module + */ +@Module({}) +export class CacheModule extends CacheModuleClass { + static register(options: CacheOptions): DynamicModule { + return super.register(options); + } + + static registerAsync(options: CacheAsyncOptions): DynamicModule { + return super.registerAsync(options); + } + + static forRoot(options: CacheOptions): DynamicModule { + return super.register({ ...options, global: true }); + } + + static forRootAsync(options: CacheAsyncOptions): DynamicModule { + return super.registerAsync({ ...options, global: true }); + } + + static forFeature(options: CacheOptions): DynamicModule { + const { entities } = options; + + if (!entities) { + throw new Error('You must provide the entities option'); + } + + return { + module: CacheModule, + imports: createCacheImports({ entities }), + providers: createCacheProviders({ entities, overrides: options }), + exports: createCacheExports(), + }; + } +} diff --git a/packages/nestjs-cache/src/cache.seeder.ts b/packages/nestjs-cache/src/cache.seeder.ts new file mode 100644 index 000000000..cf93f14f3 --- /dev/null +++ b/packages/nestjs-cache/src/cache.seeder.ts @@ -0,0 +1,23 @@ +import { Seeder } from '@concepta/typeorm-seeding'; +import { CacheFactory } from './cache.factory'; + +/** + * Cache seeder + */ +export class CacheSeeder extends Seeder { + /** + * Runner + */ + public async run(): Promise { + // number of caches to create + const createAmount = process.env?.CACHE_MODULE_SEEDER_AMOUNT + ? Number(process.env.CACHE_MODULE_SEEDER_AMOUNT) + : 50; + + // the factory + const cacheFactory = this.factory(CacheFactory); + + // create a bunch + await cacheFactory.createMany(createAmount); + } +} diff --git a/packages/nestjs-cache/src/cache.types.spec.ts b/packages/nestjs-cache/src/cache.types.spec.ts new file mode 100644 index 000000000..804f62071 --- /dev/null +++ b/packages/nestjs-cache/src/cache.types.spec.ts @@ -0,0 +1,10 @@ +import { CacheResource } from './cache.types'; + +describe('Org Types', () => { + describe('OrgResource enum', () => { + it('should match', async () => { + expect(CacheResource.One).toEqual('cache'); + expect(CacheResource.Many).toEqual('cache-list'); + }); + }); +}); diff --git a/packages/nestjs-cache/src/cache.types.ts b/packages/nestjs-cache/src/cache.types.ts new file mode 100644 index 000000000..a89ba67c8 --- /dev/null +++ b/packages/nestjs-cache/src/cache.types.ts @@ -0,0 +1,4 @@ +export enum CacheResource { + 'One' = 'cache', + 'Many' = 'cache-list', +} diff --git a/packages/nestjs-cache/src/config/cache-default.config.ts b/packages/nestjs-cache/src/config/cache-default.config.ts new file mode 100644 index 000000000..50f216cf6 --- /dev/null +++ b/packages/nestjs-cache/src/config/cache-default.config.ts @@ -0,0 +1,13 @@ +import { registerAs } from '@nestjs/config'; +import { CACHE_MODULE_DEFAULT_SETTINGS_TOKEN } from '../cache.constants'; +import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; + +/** + * Default configuration for Cache module. + */ +export const cacheDefaultConfig = registerAs( + CACHE_MODULE_DEFAULT_SETTINGS_TOKEN, + (): Partial => ({ + expiresIn: process.env.CACHE_EXPIRE_IN ? process.env.CACHE_EXPIRE_IN : null, + }), +); 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 new file mode 100644 index 000000000..8229c6faf --- /dev/null +++ b/packages/nestjs-cache/src/controllers/cache-crud.controller.e2e-spec.ts @@ -0,0 +1,172 @@ +import { CacheCreatableInterface } from '@concepta/ts-common'; +import { SeedingSource } from '@concepta/typeorm-seeding'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import assert from 'assert'; +import supertest from 'supertest'; +import { Repository } from 'typeorm'; + +import { CACHE_MODULE_REPOSITORIES_TOKEN } from '../cache.constants'; + +import { CacheFactory } from '../cache.factory'; +import { CacheSeeder } from '../cache.seeder'; + +import { AppModuleFixture } from '../__fixtures__/app.module.fixture'; + +import { UserCacheEntityFixture } from '../__fixtures__/entities/user-cache-entity.fixture'; +import { UserEntityFixture } from '../__fixtures__/entities/user-entity.fixture'; +import { UserCacheFactoryFixture } from '../__fixtures__/factories/user-cache.factory.fixture'; +import { UserFactoryFixture } from '../__fixtures__/factories/user.factory.fixture'; + +describe('CacheAssignmentController (e2e)', () => { + let app: INestApplication; + let seedingSource: SeedingSource; + let userFactory: UserFactoryFixture; + let userCacheFactory: UserCacheFactoryFixture; + let user: UserEntityFixture; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModuleFixture], + }).compile(); + app = moduleFixture.createNestApplication(); + await app.init(); + + seedingSource = new SeedingSource({ + dataSource: app.get(getDataSourceToken()), + }); + + await seedingSource.initialize(); + + userFactory = new UserFactoryFixture({ seedingSource }); + userCacheFactory = new UserCacheFactoryFixture({ seedingSource }); + + const cacheSeeder = new CacheSeeder({ + factories: [new CacheFactory({ entity: UserCacheEntityFixture })], + }); + + await seedingSource.run.one(cacheSeeder); + + user = await userFactory.create(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + return app ? await app.close() : undefined; + }); + + it('GET /cache/user', async () => { + await userCacheFactory + .map((userCache) => { + userCache.assignee = user; + }) + .createMany(2); + + await supertest(app.getHttpServer()) + .get('/cache/user?limit=2') + .expect(200) + .then((res) => { + assert.strictEqual(res.body.length, 2); + }); + }); + + it('GET /cache/user/:id', async () => { + const userCache = await userCacheFactory + .map((userCache) => { + userCache.assignee = user; + }) + .create(); + + await supertest(app.getHttpServer()) + .get( + `/cache/user/${userCache.id}` + `?filter[0]=key||$eq||${userCache.key}`, + ) + .expect(200) + .then((res) => { + assert.strictEqual(res.body.assignee.id, user.id); + }); + }); + + it('GET /cache/user/ with key and type filters', async () => { + const userCache = await userCacheFactory + .map((userCache) => { + userCache.assignee = user; + userCache.key = 'specific-key'; + userCache.type = 'specific-type'; + userCache.data = JSON.stringify({ name: 'John Doe' }); + }) + .create(); + + const url = + `/cache/user/` + + `?filter[0]=key||$eq||${userCache.key}` + + `&filter[1]=type||$eq||${userCache.type}`; + // Assuming your endpoint can filter by key and type + await supertest(app.getHttpServer()) + .get(url) + .expect(200) + .then((res) => { + const response = res.body[0]; + assert.strictEqual(response.assignee.id, user.id); + assert.strictEqual(response.key, userCache.key); + assert.strictEqual(response.type, userCache.type); + assert.strictEqual(response.data, userCache.data); + }); + }); + + it('POST /cache/user', async () => { + const payload: CacheCreatableInterface = { + key: 'dashboard-1', + type: 'filter', + data: '{}', + expiresIn: '1d', + assignee: { id: user.id }, + }; + + await supertest(app.getHttpServer()) + .post('/cache/user') + .send(payload) + .expect(201) + .then((res) => { + expect(res.body.key).toBe(payload.key); + expect(res.body.assignee.id).toBe(user.id); + }); + }); + + it.only('POST /cache/user Duplicated', async () => { + const payload: CacheCreatableInterface = { + key: 'dashboard-1', + type: 'filter', + data: '{}', + expiresIn: '1d', + assignee: { id: user.id }, + }; + + await supertest(app.getHttpServer()) + .post('/cache/user') + .send(payload) + .expect(201) + .then((res) => { + expect(res.body.key).toBe(payload.key); + expect(res.body.assignee.id).toBe(user.id); + }); + + await supertest(app.getHttpServer()) + .post('/cache/user') + .send(payload) + .expect(500); + }); + + it('DELETE /cache/user/:id', async () => { + const userCache = await userCacheFactory + .map((userCache) => { + userCache.assignee = user; + }) + .create(); + + await supertest(app.getHttpServer()) + .delete(`/cache/user/${userCache.id}`) + .expect(200); + }); +}); diff --git a/packages/nestjs-cache/src/controllers/cache-crud.controller.ts b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts new file mode 100644 index 000000000..5a948894b --- /dev/null +++ b/packages/nestjs-cache/src/controllers/cache-crud.controller.ts @@ -0,0 +1,201 @@ +import { + AccessControlCreateOne, + AccessControlDeleteOne, + AccessControlReadMany, + AccessControlReadOne, +} from '@concepta/nestjs-access-control'; +import { + CrudBody, + CrudController, + CrudControllerInterface, + CrudCreateOne, + CrudDeleteOne, + CrudReadMany, + CrudReadOne, + CrudRequest, + CrudRequestInterface, + CrudUpdateOne, +} from '@concepta/nestjs-crud'; +import { + CacheCreatableInterface, + CacheInterface, + CacheUpdatableInterface, +} from '@concepta/ts-common'; +import { ReferenceAssignment } from '@concepta/ts-core'; +import { Inject, Param } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import ms from 'ms'; +import { + CACHE_MODULE_CRUD_SERVICES_TOKEN, + CACHE_MODULE_SETTINGS_TOKEN, +} from '../cache.constants'; +import { CacheResource } from '../cache.types'; +import { CachePaginatedDto } from '../dto/cache-paginated.dto'; +import { CacheUpdateDto } from '../dto/cache-update.dto'; +import { CacheDto } from '../dto/cache.dto'; +import { CacheAssignmentNotFoundException } from '../exceptions/cache-assignment-not-found.exception'; +import { EntityNotFoundException } from '../exceptions/entity-not-found.exception'; +import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; +import { CacheCrudService } from '../services/cache-crud.service'; +import getExpirationDate from '../utils/get-expiration-date.util'; + +/** + * Cache assignment controller. + */ +@ApiTags('cache') +@CrudController({ + path: 'cache/:assignment', + model: { + type: CacheDto, + paginatedType: CachePaginatedDto, + }, + params: { + id: { field: 'id', type: 'string', primary: true }, + assignment: { + field: 'assignment', + disabled: true, + }, + }, + join: { cache: { eager: true }, assignee: { eager: true } }, +}) +export class CacheCrudController + implements + CrudControllerInterface< + CacheInterface, + CacheCreatableInterface, + CacheUpdatableInterface, + never + > +{ + /** + * Constructor. + * + * @param allCrudServices instances of all crud services + */ + constructor( + @Inject(CACHE_MODULE_SETTINGS_TOKEN) + private settings: CacheSettingsInterface, + @Inject(CACHE_MODULE_CRUD_SERVICES_TOKEN) + private allCrudServices: Record, + ) {} + + /** + * Get many + * + * @param crudRequest the CRUD request object + * @param assignment the assignment + */ + @CrudReadMany() + @AccessControlReadMany(CacheResource.Many) + async getMany( + @CrudRequest() crudRequest: CrudRequestInterface, + @Param('assignment') assignment: ReferenceAssignment, + ) { + return this.getCrudService(assignment).getMany(crudRequest); + } + + /** + * Get one + * + * @param crudRequest the CRUD request object + * @param assignment The cache assignment + */ + @CrudReadOne() + @AccessControlReadOne(CacheResource.One) + async getOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @Param('assignment') assignment: ReferenceAssignment, + ) { + return this.getCrudService(assignment).getOne(crudRequest); + } + + /** + * Create one + * + * @param crudRequest the CRUD request object + * @param cacheCreateDto cache create dto + * @param assignment The cache assignment + */ + @CrudCreateOne() + @AccessControlCreateOne(CacheResource.One) + async createOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() cacheCreateDto: CacheCreatableInterface, + @Param('assignment') assignment: ReferenceAssignment, + ) { + const expirationDate = getExpirationDate( + cacheCreateDto.expiresIn ?? this.settings.expiresIn, + ); + + // call crud service to create + return this.getCrudService(assignment).createOne(crudRequest, { + ...cacheCreateDto, + expirationDate, + }); + } + + /** + * Create one + * + * @param crudRequest the CRUD request object + * @param cacheUpdateDto cache create dto + * @param assignment The cache assignment + */ + @CrudUpdateOne() + @AccessControlCreateOne(CacheResource.One) + async updateOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @CrudBody() cacheUpdateDto: CacheUpdateDto, + @Param('assignment') assignment: ReferenceAssignment, + ) { + const expirationDate = getExpirationDate( + cacheUpdateDto.expiresIn ?? this.settings.expiresIn, + ); + + // call crud service to create + return this.getCrudService(assignment).updateOne(crudRequest, { + ...cacheUpdateDto, + expirationDate, + }); + } + + /** + * Delete one + * + * @param crudRequest the CRUD request object + * @param assignment The cache assignment + */ + @CrudDeleteOne() + @AccessControlDeleteOne(CacheResource.One) + async deleteOne( + @CrudRequest() crudRequest: CrudRequestInterface, + @Param('assignment') assignment: ReferenceAssignment, + ) { + return this.getCrudService(assignment).deleteOne(crudRequest); + } + + /** + * Get the crud service for the given assignment. + * + * @private + * @param assignment The cache assignment + */ + protected getCrudService(assignment: ReferenceAssignment): CacheCrudService { + // 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 EntityNotFoundException(entityKey); + } + } else { + // bad assignment + throw new CacheAssignmentNotFoundException(assignment); + } + } +} diff --git a/packages/nestjs-cache/src/dto/cache-create.dto.ts b/packages/nestjs-cache/src/dto/cache-create.dto.ts new file mode 100644 index 000000000..ed3818b74 --- /dev/null +++ b/packages/nestjs-cache/src/dto/cache-create.dto.ts @@ -0,0 +1,18 @@ +import { CacheCreatableInterface } from '@concepta/ts-common'; +import { PickType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { CacheDto } from './cache.dto'; +/** + * Cache Create DTO + */ +@Exclude() +export class CacheCreateDto + extends PickType(CacheDto, [ + 'key', + 'data', + 'type', + 'expiresIn', + 'assignee', + ] as const) + implements CacheCreatableInterface +{} diff --git a/packages/nestjs-cache/src/dto/cache-paginated.dto.ts b/packages/nestjs-cache/src/dto/cache-paginated.dto.ts new file mode 100644 index 000000000..5fc35cb99 --- /dev/null +++ b/packages/nestjs-cache/src/dto/cache-paginated.dto.ts @@ -0,0 +1,19 @@ +import { Exclude, Expose, Type } from 'class-transformer'; +import { CacheInterface } from '@concepta/ts-common'; +import { ApiProperty } from '@nestjs/swagger'; +import { CrudResponsePaginatedDto } from '@concepta/nestjs-crud'; +import { CacheDto } from './cache.dto'; +/** + * Org paginated DTO + */ +@Exclude() +export class CachePaginatedDto extends CrudResponsePaginatedDto { + @Expose() + @ApiProperty({ + type: CacheDto, + isArray: true, + description: 'Array of Caches', + }) + @Type(() => CacheDto) + data: CacheDto[] = []; +} diff --git a/packages/nestjs-cache/src/dto/cache-update.dto.ts b/packages/nestjs-cache/src/dto/cache-update.dto.ts new file mode 100644 index 000000000..04d384d0a --- /dev/null +++ b/packages/nestjs-cache/src/dto/cache-update.dto.ts @@ -0,0 +1,17 @@ +import { CacheUpdatableInterface } from '@concepta/ts-common'; +import { PickType } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; +import { CacheDto } from './cache.dto'; +/** + * Cache Create DTO + */ +@Exclude() +export class CacheUpdateDto + extends PickType(CacheDto, [ + 'key', + 'type', + 'assignee', + 'data', + 'expiresIn', + ] as const) + implements CacheUpdatableInterface {} diff --git a/packages/nestjs-cache/src/dto/cache.dto.ts b/packages/nestjs-cache/src/dto/cache.dto.ts new file mode 100644 index 000000000..12a1c4458 --- /dev/null +++ b/packages/nestjs-cache/src/dto/cache.dto.ts @@ -0,0 +1,59 @@ +import { Exclude, Expose, Type } from 'class-transformer'; +import { Allow, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { CacheInterface } from '@concepta/ts-common'; +import { CommonEntityDto, ReferenceIdDto } from '@concepta/nestjs-common'; + +/** + * Cache Create DTO + */ +@Exclude() +export class CacheDto extends CommonEntityDto implements CacheInterface { + /** + * key + */ + @Expose() + @IsString() + key = ''; + + /** + * data + */ + @Expose() + @IsString() + @IsOptional() + data!: string | null; + + /** + * type + */ + @Expose() + @IsString() + type = ''; + + /** + * Expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). + * + * Eg: 60, "2 days", "10h", "7d" + */ + @Expose() + @IsString() + @IsOptional() + expiresIn!: string | null; + + /** + * Assignee + */ + @Expose() + @Type(() => ReferenceIdDto) + @ValidateNested() + assignee: ReferenceIdInterface = new ReferenceIdDto(); + + /** + * expirationDate + */ + @Allow() + @Type(() => Date) + @IsOptional() + expirationDate!: Date | null; +} diff --git a/packages/nestjs-cache/src/entities/cache-postgres.entity.ts b/packages/nestjs-cache/src/entities/cache-postgres.entity.ts new file mode 100644 index 000000000..3fd81cc27 --- /dev/null +++ b/packages/nestjs-cache/src/entities/cache-postgres.entity.ts @@ -0,0 +1,30 @@ +import { Column, Index, Unique } from 'typeorm'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { CacheInterface } from '@concepta/ts-common'; +import { CommonPostgresEntity } from '@concepta/typeorm-common'; + +/** + * Cache Postgres Entity + */ +@Index('key_unique_index', ['key', 'type', 'assignee.id'], { unique: true }) +export abstract class CachePostgresEntity + extends CommonPostgresEntity + implements CacheInterface +{ + @Column() + type!: string; + + @Column() + key!: string; + + @Column({ type: 'jsonb', nullable: true }) + data!: string | null; + + @Column({ type: 'timestamptz', nullable: true }) + expirationDate!: Date | null; + + /** + * Should be overwrite by the table it will be assigned to + */ + assignee!: ReferenceIdInterface; +} diff --git a/packages/nestjs-cache/src/entities/cache-sqlite.entity.ts b/packages/nestjs-cache/src/entities/cache-sqlite.entity.ts new file mode 100644 index 000000000..1854c5527 --- /dev/null +++ b/packages/nestjs-cache/src/entities/cache-sqlite.entity.ts @@ -0,0 +1,31 @@ +import { Column, Index } from 'typeorm'; +import { CommonSqliteEntity } from '@concepta/typeorm-common'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { CacheInterface } from '@concepta/ts-common'; + +/** + * Cache Sqlite Entity + */ + +@Index('key_unique_index', ['key', 'type', 'assignee.id'], { unique: true }) +export abstract class CacheSqliteEntity + extends CommonSqliteEntity + implements CacheInterface +{ + @Column() + key!: string; + + @Column() + type!: string; + + @Column({ type: 'text', nullable: true }) + data!: string; + + @Column({ type: 'datetime', nullable: true }) + expirationDate!: Date | null; + + /** + * Should be overwrite by the table it will be assigned to + */ + assignee!: ReferenceIdInterface; +} diff --git a/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.spec.ts b/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.spec.ts new file mode 100644 index 000000000..c174560bd --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.spec.ts @@ -0,0 +1,26 @@ +import { CacheAssignmentNotFoundException } from "./cache-assignment-not-found.exception"; + +describe('AssignmentNotFoundException', () => { + it('should create an instance with default message', () => { + const assignmentName = 'testAssignment'; + const exception = new CacheAssignmentNotFoundException(assignmentName); + + expect(exception).toBeInstanceOf(Error); + expect(exception.message).toBe( + 'Assignment testAssignment was not registered to be used.', + ); + expect(exception.context).toEqual({ assignmentName: 'testAssignment' }); + expect(exception.errorCode).toBe('CACHE_ASSIGNMENT_NOT_FOUND_ERROR'); + }); + + it('should create an instance with custom message', () => { + const assignmentName = 'testAssignment'; + const customMessage = 'Custom message for %s'; + const exception = new CacheAssignmentNotFoundException( + assignmentName, + customMessage, + ); + + expect(exception.message).toBe('Custom message for testAssignment'); + }); +}); diff --git a/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.ts b/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.ts new file mode 100644 index 000000000..5daa51d1d --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-assignment-not-found.exception.ts @@ -0,0 +1,23 @@ +import { format } from 'util'; +import { ExceptionInterface } from '@concepta/ts-core'; + +export class CacheAssignmentNotFoundException + extends Error + implements ExceptionInterface +{ + errorCode = 'CACHE_ASSIGNMENT_NOT_FOUND_ERROR'; + + context: { + assignmentName: string; + }; + + constructor( + assignmentName: string, + message = 'Assignment %s was not registered to be used.', + ) { + super(format(message, assignmentName)); + this.context = { + assignmentName, + }; + } +} diff --git a/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.spec.ts b/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.spec.ts new file mode 100644 index 000000000..4ca8d825a --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.spec.ts @@ -0,0 +1,25 @@ +import { CacheTypeNotDefinedException } from './cache-type-not-defined.exception'; + +describe(CacheTypeNotDefinedException.name, () => { + it('should create an instance of CacheTypeNotDefinedException', () => { + const exception = new CacheTypeNotDefinedException('test'); + expect(exception).toBeInstanceOf(CacheTypeNotDefinedException); + }); + + it('should have the correct error code', () => { + const exception = new CacheTypeNotDefinedException('test'); + expect(exception.errorCode).toBe('CACHE_TYPE_NOT_DEFINED_ERROR'); + }); + + it('should have the correct context', () => { + const exception = new CacheTypeNotDefinedException('test'); + expect(exception.context).toEqual({ type: 'test' }); + }); + + it('should have the correct message', () => { + const exception = new CacheTypeNotDefinedException('test'); + expect(exception.message).toBe( + 'Type test was not defined to be used. please check config.', + ); + }); +}); \ No newline at end of file diff --git a/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.ts b/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.ts new file mode 100644 index 000000000..c347e9e39 --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/cache-type-not-defined.exception.ts @@ -0,0 +1,23 @@ +import { format } from 'util'; +import { ExceptionInterface } from '@concepta/ts-core'; + +export class CacheTypeNotDefinedException + extends Error + implements ExceptionInterface +{ + errorCode = 'CACHE_TYPE_NOT_DEFINED_ERROR'; + + context: { + type: string; + }; + + constructor( + type: string, + message = 'Type %s was not defined to be used. please check config.', + ) { + super(format(message, type)); + this.context = { + type, + }; + } +} diff --git a/packages/nestjs-cache/src/exceptions/entity-not-found.exception.spec.ts b/packages/nestjs-cache/src/exceptions/entity-not-found.exception.spec.ts new file mode 100644 index 000000000..4465d79f2 --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/entity-not-found.exception.spec.ts @@ -0,0 +1,25 @@ +import { EntityNotFoundException } from './entity-not-found.exception'; + +describe(EntityNotFoundException.name, () => { + it('should create an instance of EntityNotFoundException', () => { + const exception = new EntityNotFoundException('TestEntity'); + expect(exception).toBeInstanceOf(EntityNotFoundException); + }); + + it('should have the correct error message', () => { + const exception = new EntityNotFoundException('TestEntity'); + expect(exception.message).toBe( + 'Entity TestEntity was not registered to be used.', + ); + }); + + it('should have the correct context', () => { + const exception = new EntityNotFoundException('TestEntity'); + expect(exception.context).toEqual({ entityName: 'TestEntity' }); + }); + + it('should have the correct error code', () => { + const exception = new EntityNotFoundException('TestEntity'); + expect(exception.errorCode).toBe('CACHE_ENTITY_NOT_FOUND_ERROR'); + }); +}); diff --git a/packages/nestjs-cache/src/exceptions/entity-not-found.exception.ts b/packages/nestjs-cache/src/exceptions/entity-not-found.exception.ts new file mode 100644 index 000000000..f7e0b05d7 --- /dev/null +++ b/packages/nestjs-cache/src/exceptions/entity-not-found.exception.ts @@ -0,0 +1,23 @@ +import { format } from 'util'; +import { ExceptionInterface } from '@concepta/ts-core'; + +export class EntityNotFoundException + extends Error + implements ExceptionInterface +{ + errorCode = 'CACHE_ENTITY_NOT_FOUND_ERROR'; + + context: { + entityName: string; + }; + + constructor( + entityName: string, + message = 'Entity %s was not registered to be used.', + ) { + super(format(message, entityName)); + this.context = { + entityName, + }; + } +} diff --git a/packages/nestjs-cache/src/index.spec.ts b/packages/nestjs-cache/src/index.spec.ts new file mode 100644 index 000000000..95871f85b --- /dev/null +++ b/packages/nestjs-cache/src/index.spec.ts @@ -0,0 +1,29 @@ +import { + CacheModule, + CacheService, + CachePostgresEntity, + CacheSqliteEntity, + CacheCreateDto, +} from './index'; + +describe('index', () => { + it('should be an instance of Function', () => { + expect(CacheModule).toBeInstanceOf(Function); + }); + + it('should be an instance of Function', () => { + expect(CacheService).toBeInstanceOf(Function); + }); + + it('should be an instance of Function', () => { + expect(CachePostgresEntity).toBeInstanceOf(Function); + }); + + it('should be an instance of Function', () => { + expect(CacheSqliteEntity).toBeInstanceOf(Function); + }); + + it('should be an instance of Function', () => { + expect(CacheCreateDto).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-cache/src/index.ts b/packages/nestjs-cache/src/index.ts new file mode 100644 index 000000000..70c3ad86b --- /dev/null +++ b/packages/nestjs-cache/src/index.ts @@ -0,0 +1,9 @@ +export { CacheModule } from './cache.module'; + +export { CachePostgresEntity } from './entities/cache-postgres.entity'; +export { CacheSqliteEntity } from './entities/cache-sqlite.entity'; + +export { CacheService } from './services/cache.service'; +export { CacheCreateDto } from './dto/cache-create.dto'; +export { CacheUpdateDto } from './dto/cache-update.dto'; +export { CacheDto } from './dto/cache.dto'; diff --git a/packages/nestjs-cache/src/interfaces/cache-entities-options.interface.ts b/packages/nestjs-cache/src/interfaces/cache-entities-options.interface.ts new file mode 100644 index 000000000..bffbcd5ac --- /dev/null +++ b/packages/nestjs-cache/src/interfaces/cache-entities-options.interface.ts @@ -0,0 +1,5 @@ +import { TypeOrmExtEntityOptionInterface } from '@concepta/nestjs-typeorm-ext'; + +export interface CacheEntitiesOptionsInterface { + entities: Record; +} diff --git a/packages/nestjs-cache/src/interfaces/cache-options-extras.interface.ts b/packages/nestjs-cache/src/interfaces/cache-options-extras.interface.ts new file mode 100644 index 000000000..6c3623e3f --- /dev/null +++ b/packages/nestjs-cache/src/interfaces/cache-options-extras.interface.ts @@ -0,0 +1,6 @@ +import { DynamicModule } from '@nestjs/common'; +import { CacheEntitiesOptionsInterface } from './cache-entities-options.interface'; + +export interface CacheOptionsExtrasInterface + extends Pick, + Partial {} diff --git a/packages/nestjs-cache/src/interfaces/cache-options.interface.ts b/packages/nestjs-cache/src/interfaces/cache-options.interface.ts new file mode 100644 index 000000000..236ef8e4a --- /dev/null +++ b/packages/nestjs-cache/src/interfaces/cache-options.interface.ts @@ -0,0 +1,5 @@ +import { CacheSettingsInterface } from './cache-settings.interface'; + +export interface CacheOptionsInterface { + settings?: CacheSettingsInterface; +} diff --git a/packages/nestjs-cache/src/interfaces/cache-service.interface.ts b/packages/nestjs-cache/src/interfaces/cache-service.interface.ts new file mode 100644 index 000000000..00eb3407b --- /dev/null +++ b/packages/nestjs-cache/src/interfaces/cache-service.interface.ts @@ -0,0 +1,23 @@ +import { ReferenceAssignment } from '@concepta/ts-core'; +import { QueryOptionsInterface } from '@concepta/typeorm-common'; +import { + CacheClearInterface, + CacheCreateInterface, + CacheDeleteInterface, + CacheGetOneInterface, + CacheInterface, + CacheUpdateInterface, +} from '@concepta/ts-common'; + +export interface CacheServiceInterface + extends CacheCreateInterface, + CacheDeleteInterface, + CacheUpdateInterface, + CacheGetOneInterface, + CacheClearInterface { + getAssignedCaches( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise; +} diff --git a/packages/nestjs-cache/src/interfaces/cache-settings.interface.ts b/packages/nestjs-cache/src/interfaces/cache-settings.interface.ts new file mode 100644 index 000000000..40c8dbf55 --- /dev/null +++ b/packages/nestjs-cache/src/interfaces/cache-settings.interface.ts @@ -0,0 +1,6 @@ +import { LiteralObject } from '@concepta/ts-core'; + +export interface CacheSettingsInterface { + assignments: LiteralObject<{ entityKey: string }>; + expiresIn?: string | undefined | null; +} diff --git a/packages/nestjs-cache/src/seeding.ts b/packages/nestjs-cache/src/seeding.ts new file mode 100644 index 000000000..11d83537c --- /dev/null +++ b/packages/nestjs-cache/src/seeding.ts @@ -0,0 +1,7 @@ +/** + * These exports all you to import seeding related classes + * and tools without loading the entire module which + * runs all of it's decorators and meta data. + */ +export { CacheFactory } from './cache.factory'; +export { CacheSeeder } from './cache.seeder'; diff --git a/packages/nestjs-cache/src/services/cache-crud.service.ts b/packages/nestjs-cache/src/services/cache-crud.service.ts new file mode 100644 index 000000000..54603b677 --- /dev/null +++ b/packages/nestjs-cache/src/services/cache-crud.service.ts @@ -0,0 +1,18 @@ +import { TypeOrmCrudService } from '@concepta/nestjs-crud'; +import { CacheInterface } from '@concepta/ts-common'; +import { Injectable } from '@nestjs/common'; +import { Repository } from 'typeorm'; +/** + * Cache CRUD service + */ +@Injectable() +export class CacheCrudService extends TypeOrmCrudService { + /** + * Constructor + * + * @param repo instance of the cache repository. + */ + constructor(repo: Repository) { + super(repo); + } +} diff --git a/packages/nestjs-cache/src/services/cache.service.spec.ts b/packages/nestjs-cache/src/services/cache.service.spec.ts new file mode 100644 index 000000000..d81be2ef0 --- /dev/null +++ b/packages/nestjs-cache/src/services/cache.service.spec.ts @@ -0,0 +1,153 @@ +import { CacheCreatableInterface, CacheInterface } from '@concepta/ts-common'; +import { + QueryOptionsInterface, + ReferenceMutateException, + ReferenceValidationException, + RepositoryProxy, +} from '@concepta/typeorm-common'; +import { mock } from 'jest-mock-extended'; +import { CacheService } from './cache.service'; +import { Repository } from 'typeorm'; +import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; + +import { CacheCreateDto } from '../dto/cache-create.dto'; +import { ReferenceAssignment } from '@concepta/ts-core'; +import getExpirationDate from '../utils/get-expiration-date.util'; + +const expirationDate = new Date(); +expirationDate.setHours(expirationDate.getHours() + 1); + +jest.mock('../utils/get-expiration-date.util', () => ({ + __esModule: true, + default: jest.fn(() => expirationDate), +})); + +describe('CacheService', () => { + let service: CacheService; + let repo: Repository; + let settings: CacheSettingsInterface; + const cacheDto: CacheCreatableInterface = { + key: 'testKey', + type: 'testType', + data: 'testData', + assignee: { id: 'testAssignee' }, + expiresIn: '1h', + }; + + const queryOptions: QueryOptionsInterface = {}; + const assignment: ReferenceAssignment = 'testAssignment'; + const cacheCreateDto = new CacheCreateDto(); + const repoProxyMock = mock>(); + // const expirationDate = new Date(); + + const cacheEntity: CacheInterface = { + ...cacheDto, + expirationDate, + id: 'testId', + dateCreated: new Date(), + dateUpdated: new Date(), + dateDeleted: null, + version: 1, + }; + beforeEach(() => { + repo = mock>(); + settings = mock(); + settings.expiresIn = '1h'; + service = new CacheService({ testAssignment: repo }, settings); + }); + + describe(CacheService.prototype.create, () => { + it('should create a cache entry', async () => { + Object.assign(cacheCreateDto, cacheDto); + + repoProxyMock.repository.mockReturnValue(repo); + + // Mocking validateDto method + service['validateDto'] = jest.fn().mockResolvedValue(cacheCreateDto); + + // Mocking RepositoryProxy class + jest.spyOn(RepositoryProxy.prototype, 'repository').mockReturnValue(repo); + + await service.create(assignment, cacheDto, queryOptions); + + expect(repo.save).toHaveBeenCalledWith({ + key: cacheDto.key, + type: cacheDto.type, + data: cacheDto.data, + assignee: cacheDto.assignee, + expirationDate, + }); + }); + + it('should throw a ReferenceValidationException on error', async () => { + const assignment: ReferenceAssignment = 'testAssignment'; + + const error = new ReferenceValidationException('error', []); + service['validateDto'] = jest.fn().mockRejectedValue(error); + + await expect( + service.create(assignment, cacheDto, queryOptions), + ).rejects.toThrow(ReferenceValidationException); + }); + + it('should throw a ReferenceMutateException on error', async () => { + const assignment: ReferenceAssignment = 'testAssignment'; + + jest + .spyOn(RepositoryProxy.prototype, 'repository') + .mockImplementationOnce(() => { + throw new Error(); + }); + + const t = async () => + await service.create(assignment, cacheDto, queryOptions); + expect(t).rejects.toThrow(ReferenceMutateException); + }); + }); + + describe(CacheService.prototype.update, () => { + it('should update a cache entry', async () => { + Object.assign(cacheCreateDto, cacheDto); + + repoProxyMock.repository.mockReturnValue(repo); + + service['validateDto'] = jest.fn().mockResolvedValueOnce(cacheDto); + service['findCache'] = jest.fn(); + const result = { + key: cacheDto.key, + type: cacheDto.type, + data: cacheDto.data, + assignee: cacheDto.assignee, + expirationDate, + }; + service['mergeEntity'] = jest.fn().mockResolvedValue(result); + + jest.spyOn(RepositoryProxy.prototype, 'repository').mockReturnValue(repo); + + await service.update(assignment, cacheDto, queryOptions); + + expect(repo.save).toHaveBeenCalledWith(result); + }); + + it('should throw a ReferenceValidationException on error', async () => { + const assignment: ReferenceAssignment = 'testAssignment'; + + const error = new ReferenceValidationException('error', []); + service['validateDto'] = jest.fn().mockRejectedValue(error); + + await expect( + service.update(assignment, cacheDto, queryOptions), + ).rejects.toThrow(ReferenceValidationException); + }); + + it('should throw a ReferenceMutateException on error', async () => { + const assignment: ReferenceAssignment = 'testAssignment'; + + const error = new Error('error'); + service['mergeEntity'] = jest.fn().mockResolvedValue(error); + + const t = () => service.update(assignment, cacheDto, queryOptions); + await expect(t).rejects.toThrow(ReferenceMutateException); + }); + }); +}); diff --git a/packages/nestjs-cache/src/services/cache.service.ts b/packages/nestjs-cache/src/services/cache.service.ts new file mode 100644 index 000000000..7de29f372 --- /dev/null +++ b/packages/nestjs-cache/src/services/cache.service.ts @@ -0,0 +1,317 @@ +import { + CacheCreatableInterface, + CacheInterface, + CacheUpdatableInterface, +} from '@concepta/ts-common'; +import { ReferenceAssignment, ReferenceId, Type } from '@concepta/ts-core'; +import { + QueryOptionsInterface, + ReferenceLookupException, + ReferenceMutateException, + ReferenceValidationException, + RepositoryProxy, +} from '@concepta/typeorm-common'; +import { Inject, Injectable } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import ms from 'ms'; +import { DeepPartial, Repository } from 'typeorm'; +import { + CACHE_MODULE_REPOSITORIES_TOKEN, + CACHE_MODULE_SETTINGS_TOKEN, +} from '../cache.constants'; +import { CacheCreateDto } from '../dto/cache-create.dto'; +import { CacheUpdateDto } from '../dto/cache-update.dto'; +import { EntityNotFoundException } from '../exceptions/entity-not-found.exception'; +import { CacheServiceInterface } from '../interfaces/cache-service.interface'; +import { CacheSettingsInterface } from '../interfaces/cache-settings.interface'; +import getExpirationDate from '../utils/get-expiration-date.util'; + +@Injectable() +export class CacheService implements CacheServiceInterface { + constructor( + @Inject(CACHE_MODULE_REPOSITORIES_TOKEN) + private allCacheRepos: Record>, + @Inject(CACHE_MODULE_SETTINGS_TOKEN) + protected readonly settings: CacheSettingsInterface, + ) {} + + /** + * Create a cache with a for the given assignee. + * + * @param assignment The cache assignment + * @param cache The data to create + */ + async create( + assignment: ReferenceAssignment, + cache: CacheCreatableInterface, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get the assignment repo + const assignmentRepo = this.getAssignmentRepo(assignment); + + // validate the data + const dto = await this.validateDto(CacheCreateDto, cache); + + // break out the vars + const { key, type, data, assignee, expiresIn } = dto; + + // try to find the relationship + try { + // generate the expiration date + const expirationDate = getExpirationDate( + expiresIn ?? this.settings.expiresIn, + ); + + // new repo proxy + const repoProxy = new RepositoryProxy(assignmentRepo); + + // try to save the item + return repoProxy.repository(queryOptions).save({ + key, + type, + data, + assignee, + expirationDate, + }); + } catch (e) { + throw new ReferenceMutateException(assignmentRepo.metadata.targetName, e); + } + } + + async update( + assignment: ReferenceAssignment, + cache: CacheUpdatableInterface, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get the assignment repo + const assignmentRepo = this.getAssignmentRepo(assignment); + + // validate the data + const dto = await this.validateDto(CacheUpdateDto, cache); + + // generate the expiration date + const expirationDate = getExpirationDate( + dto.expiresIn ?? this.settings.expiresIn, + ); + // new repo proxy + const repoProxy = new RepositoryProxy(assignmentRepo); + + // try to update the item + try { + const assignedCache = await this.findCache(repoProxy, dto, queryOptions); + + const mergedEntity = await this.mergeEntity( + repoProxy, + assignedCache, + dto, + queryOptions, + ); + + return repoProxy.repository(queryOptions).save({ + ...mergedEntity, + expirationDate, + }); + } catch (e) { + throw new ReferenceMutateException(assignmentRepo.metadata.targetName, e); + } + } + + /** + * Delete a cache based on params + * + * @param assignment The cache assignment + * @param cache The cache to delete + */ + async delete( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get cache from an assigned user for a category + const assignedCache = await this.get(assignment, cache, queryOptions); + + if (assignedCache) { + this.deleteCache(assignment, assignedCache.id, queryOptions); + } + } + + /** + * Get all CACHEs for assignee. + * + * @param assignment The assignment of the check + * @param cache The cache to get assignments + */ + async getAssignedCaches( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get the assignment repo + const assignmentRepo = this.getAssignmentRepo(assignment); + + // break out the args + const { assignee } = cache; + + // new repo proxy + const repoProxy = new RepositoryProxy(assignmentRepo); + + // try to find the relationships + try { + // make the query + const assignments = await repoProxy.repository(queryOptions).find({ + where: { + assignee: { id: assignee.id }, + }, + relations: ['assignee'], + }); + + // return the caches from assignee + return assignments; + } catch (e) { + throw new ReferenceLookupException(assignmentRepo.metadata.targetName, e); + } + } + + async get( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get the assignment repo + const assignmentRepo = this.getAssignmentRepo(assignment); + + // new repo proxy + const repoProxy = new RepositoryProxy(assignmentRepo); + + return await this.findCache(repoProxy, cache, queryOptions); + } + + /** + * Clear all caches for a given assignee. + * + * @param assignment The assignment of the repository + * @param cache The cache to clear + */ + async clear( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise { + // get all caches from an assigned user for a category + const assignedCaches = await this.getAssignedCaches( + assignment, + cache, + queryOptions, + ); + + // Map to get ids + const assignedCacheIds = assignedCaches.map( + (assignedCache) => assignedCache.id, + ); + + if (assignedCacheIds.length > 0) + await this.deleteCache(assignment, assignedCacheIds, queryOptions); + } + + /** + * Delete CACHE based on assignment + * + * @private + * @param assignment The assignment to delete id from + * @param id The id or ids to delete + */ + protected async deleteCache( + assignment: ReferenceAssignment, + id: ReferenceId | ReferenceId[], + queryOptions?: QueryOptionsInterface, + ): Promise { + // get the assignment repo + const assignmentRepo = this.getAssignmentRepo(assignment); + + // new repo proxy + const repoProxy = new RepositoryProxy(assignmentRepo); + + try { + await repoProxy.repository(queryOptions).delete(id); + } catch (e) { + throw new ReferenceMutateException(assignmentRepo.metadata.targetName, e); + } + } + + // Should this be on nestjs-common? + protected async validateDto>( + type: Type, + data: T, + ): Promise { + // convert to dto + const dto = plainToInstance(type, data); + + // validate the data + const validationErrors = await validate(dto); + + // any errors? + if (validationErrors.length) { + // yes, throw error + throw new ReferenceValidationException( + this.constructor.name, + validationErrors, + ); + } + + return dto; + } + + protected async findCache( + repoProxy: RepositoryProxy, + cache: Pick, + queryOptions?: QueryOptionsInterface, + ): Promise { + const { key, type, assignee } = cache; + try { + const cache = await repoProxy.repository(queryOptions).findOne({ + where: { + key, + type, + assignee, + }, + relations: ['assignee'], + }); + if (!cache) throw new Error('Could not find repository'); + return cache; + } catch (e) { + throw new ReferenceLookupException( + repoProxy.repository(queryOptions).metadata.targetName, + e, + ); + } + } + + /** + * Get the assignment repo for the given assignment. + * + * @private + * @param assignment The cache assignment + */ + protected getAssignmentRepo( + assignment: ReferenceAssignment, + ): Repository { + // repo matching assignment was injected? + if (this.allCacheRepos[assignment]) { + // yes, return it + return this.allCacheRepos[assignment]; + } else { + // bad assignment + throw new EntityNotFoundException(assignment); + } + } + + private async mergeEntity( + repoProxy: RepositoryProxy, + assignedCache: CacheInterface, + dto: CacheUpdateDto, + queryOptions?: QueryOptionsInterface, + ): Promise { + return repoProxy.repository(queryOptions).merge(assignedCache, dto); + } +} diff --git a/packages/nestjs-cache/src/utils/get-expiration-date.util.ts b/packages/nestjs-cache/src/utils/get-expiration-date.util.ts new file mode 100644 index 000000000..294e606bf --- /dev/null +++ b/packages/nestjs-cache/src/utils/get-expiration-date.util.ts @@ -0,0 +1,17 @@ +import ms from 'ms'; + +const getExpirationDate = ( + expiresIn: string | null | undefined, +): Date | null => { + if (!expiresIn) return null; + + const now = new Date(); + const expires = ms(expiresIn); + + if (!expires) throw new Error(`Invalid expiresIn`); + + // add time in seconds to now as string format + return new Date(now.getTime() + expires); +}; + +export default getExpirationDate; diff --git a/packages/nestjs-cache/tsconfig.json b/packages/nestjs-cache/tsconfig.json new file mode 100644 index 000000000..ef9980950 --- /dev/null +++ b/packages/nestjs-cache/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist", + "typeRoots": [ + "./node_modules/@types", + "../../node_modules/@types" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/ts-common/src/cache/interfaces/cache-clear.interface.ts b/packages/ts-common/src/cache/interfaces/cache-clear.interface.ts new file mode 100644 index 000000000..a4c45f3b4 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-clear.interface.ts @@ -0,0 +1,21 @@ +import { + ReferenceAssignment, + ReferenceQueryOptionsInterface, +} from '@concepta/ts-core'; +import { CacheInterface } from './cache.interface'; + +export interface CacheClearInterface< + O extends ReferenceQueryOptionsInterface = ReferenceQueryOptionsInterface, +> { + /** + * Clear all caches for assign in given category. + * + * @param assignment The assignment of the repository + * @param cache The cache to clear + */ + clear( + assignment: ReferenceAssignment, + cache: Pick, + options?: O, + ): Promise; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-creatable.interface.ts b/packages/ts-common/src/cache/interfaces/cache-creatable.interface.ts new file mode 100644 index 000000000..2c48cedc4 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-creatable.interface.ts @@ -0,0 +1,6 @@ +import { CacheInterface } from './cache.interface'; + +export interface CacheCreatableInterface + extends Pick { + expiresIn: string | null; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-create.interface.ts b/packages/ts-common/src/cache/interfaces/cache-create.interface.ts new file mode 100644 index 000000000..e4dbb862f --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-create.interface.ts @@ -0,0 +1,23 @@ +import { + ReferenceAssignment, + ReferenceQueryOptionsInterface, +} from '@concepta/ts-core'; + +import { CacheCreatableInterface } from './cache-creatable.interface'; +import { CacheInterface } from './cache.interface'; + +export interface CacheCreateInterface< + O extends ReferenceQueryOptionsInterface = ReferenceQueryOptionsInterface, +> { + /** + * Create a cache with a for the given assignee. + * + * @param assignment The cache assignment + * @param cache The CACHE to create + */ + create( + assignment: ReferenceAssignment, + cache: CacheCreatableInterface, + options?: O, + ): Promise; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-delete.interface.ts b/packages/ts-common/src/cache/interfaces/cache-delete.interface.ts new file mode 100644 index 000000000..d5f762640 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-delete.interface.ts @@ -0,0 +1,20 @@ +import { + ReferenceAssignment, + ReferenceQueryOptionsInterface, +} from '@concepta/ts-core'; +import { CacheInterface } from './cache.interface'; + +export interface CacheDeleteInterface< + O extends ReferenceQueryOptionsInterface = ReferenceQueryOptionsInterface, +> { + /** + * Delete a cache based on params + * @param assignment The cache assignment + * @param cache The dto with unique keys to delete + */ + delete( + assignment: ReferenceAssignment, + cache: Pick, + options?: O, + ): Promise; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-get-one.interface.ts b/packages/ts-common/src/cache/interfaces/cache-get-one.interface.ts new file mode 100644 index 000000000..30e2c3be4 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-get-one.interface.ts @@ -0,0 +1,20 @@ +import { + ReferenceAssignment, + ReferenceQueryOptionsInterface, +} from '@concepta/ts-core'; +import { CacheInterface } from './cache.interface'; + +export interface CacheGetOneInterface< + O extends ReferenceQueryOptionsInterface = ReferenceQueryOptionsInterface, +> { + /** + * Get One cache based on params + * @param assignment The cache assignment + * @param cache The dto with unique keys to delete + */ + get( + assignment: ReferenceAssignment, + cache: Pick, + queryOptions?: O, + ): Promise; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-updatable.interface.ts b/packages/ts-common/src/cache/interfaces/cache-updatable.interface.ts new file mode 100644 index 000000000..768965b5d --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-updatable.interface.ts @@ -0,0 +1,6 @@ +import { CacheInterface } from './cache.interface'; + +export interface CacheUpdatableInterface + extends Pick { + expiresIn: string | null; +} diff --git a/packages/ts-common/src/cache/interfaces/cache-update.interface.ts b/packages/ts-common/src/cache/interfaces/cache-update.interface.ts new file mode 100644 index 000000000..789d44891 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache-update.interface.ts @@ -0,0 +1,21 @@ +import { + ReferenceAssignment, + ReferenceQueryOptionsInterface, +} from '@concepta/ts-core'; +import { CacheUpdatableInterface } from './cache-updatable.interface'; +import { CacheInterface } from './cache.interface'; + +export interface CacheUpdateInterface< + O extends ReferenceQueryOptionsInterface = ReferenceQueryOptionsInterface, +> { + /** + * Update a cache based on params + * @param assignment The cache assignment + * @param cache The dto with unique keys to delete + */ + update( + assignment: ReferenceAssignment, + cache: CacheUpdatableInterface, + options?: O, + ): Promise; +} diff --git a/packages/ts-common/src/cache/interfaces/cache.interface.ts b/packages/ts-common/src/cache/interfaces/cache.interface.ts new file mode 100644 index 000000000..ab757b146 --- /dev/null +++ b/packages/ts-common/src/cache/interfaces/cache.interface.ts @@ -0,0 +1,30 @@ +import { + AuditInterface, + ReferenceAssigneeInterface, + ReferenceIdInterface, +} from '@concepta/ts-core'; + +export interface CacheInterface + extends ReferenceIdInterface, + ReferenceAssigneeInterface, + AuditInterface { + /** + * key to be used as reference for the cache data + */ + key: string; + + /** + * Type of the passcode + */ + type: string; + + /** + * data of the cache + */ + data: string | null; + + /** + * Date it will expire + */ + expirationDate: Date | null; +} diff --git a/packages/ts-common/src/index.ts b/packages/ts-common/src/index.ts index 2d571fb8b..d65ceb3fb 100644 --- a/packages/ts-common/src/index.ts +++ b/packages/ts-common/src/index.ts @@ -41,6 +41,15 @@ export { OtpValidateInterface } from './otp/interfaces/otp-validate.interface'; export { OtpDeleteInterface } from './otp/interfaces/otp-delete.interface'; export { OtpClearInterface } from './otp/interfaces/otp-clear.interface'; +export { CacheInterface } from './cache/interfaces/cache.interface'; +export { CacheCreatableInterface } from './cache/interfaces/cache-creatable.interface'; +export { CacheCreateInterface } from './cache/interfaces/cache-create.interface'; +export { CacheDeleteInterface } from './cache/interfaces/cache-delete.interface'; +export { CacheClearInterface } from './cache/interfaces/cache-clear.interface'; +export { CacheUpdateInterface } from './cache/interfaces/cache-update.interface'; +export { CacheGetOneInterface } from './cache/interfaces/cache-get-one.interface'; +export { CacheUpdatableInterface } from './cache/interfaces/cache-updatable.interface'; + export { InvitationInterface } from './invitation/interfaces/invitation.interface'; export { InvitationAcceptedEventPayloadInterface } from './invitation/interfaces/invitation-accepted-event-payload.interface'; export { InvitationGetUserEventPayloadInterface } from './invitation/interfaces/invitation-get-user-event-payload.interface';