From 2ffdb706a96e044d62d3c59f23e93586d40bdf00 Mon Sep 17 00:00:00 2001 From: Sergii Mykyteiek Date: Mon, 4 Oct 2021 12:56:10 +0300 Subject: [PATCH] LT-4: Implement ttl get, add, remove endpoints --- package-lock.json | 52 ++++++++++++++++--- package.json | 2 + src/app.dto.ts | 12 +++++ src/app.module.ts | 2 + src/common/schemas/add-ttl-item.json | 15 ++++++ src/common/schemas/index.ts | 3 +- src/modules/lifo/lifo.controller.ts | 4 +- src/modules/ttl/ttl.controller.ts | 61 ++++++++++++++++++++++ src/modules/ttl/ttl.messages.ts | 4 ++ src/modules/ttl/ttl.module.ts | 12 +++++ src/modules/ttl/ttl.repository.ts | 61 ++++++++++++++++++++++ src/modules/ttl/ttl.service.ts | 70 ++++++++++++++++++++++++++ src/shared/logger/index.ts | 1 + src/shared/logger/logger.interfaces.ts | 9 ++++ src/shared/logger/logger.service.ts | 9 ++++ 15 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 src/common/schemas/add-ttl-item.json create mode 100644 src/modules/ttl/ttl.controller.ts create mode 100644 src/modules/ttl/ttl.messages.ts create mode 100644 src/modules/ttl/ttl.module.ts create mode 100644 src/modules/ttl/ttl.repository.ts create mode 100644 src/modules/ttl/ttl.service.ts create mode 100644 src/shared/logger/index.ts create mode 100644 src/shared/logger/logger.interfaces.ts create mode 100644 src/shared/logger/logger.service.ts diff --git a/package-lock.json b/package-lock.json index d0fc0c2..40a708f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/swagger": "^4.7.12", "ajv": "^7.0.3", "ajv-formats": "1.5.1", + "cache-manager": "^3.4.4", "class-transformer": "^0.4.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -25,6 +26,7 @@ "@nestjs/cli": "^7.5.1", "@nestjs/schematics": "^7.1.3", "@nestjs/testing": "^7.5.1", + "@types/cache-manager": "^3.4.2", "@types/express": "^4.17.8", "@types/jest": "^26.0.15", "@types/node": "^14.14.6", @@ -2126,6 +2128,12 @@ "@types/node": "*" } }, + "node_modules/@types/cache-manager": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.2.tgz", + "integrity": "sha512-1IwA74t5ID4KWo0Kndal16MhiPSZgMe1fGc+MLT6j5r+Ab7jku36PFTl4PP6MiWw0BJscM9QpZEo00qixNQoRg==", + "dev": true + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -2768,6 +2776,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3217,6 +3230,16 @@ "node": ">=0.10.0" } }, + "node_modules/cache-manager": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.4.tgz", + "integrity": "sha512-oayy7ukJqNlRUYNUfQBwGOLilL0X5q7GpuaF19Yqwo6qdx49OoTZKRIF5qbbr+Ru8mlTvOpvnMvVq6vw72pOPg==", + "dependencies": { + "async": "3.2.0", + "lodash": "^4.17.21", + "lru-cache": "6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -7143,7 +7166,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10985,8 +11007,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -12685,6 +12706,12 @@ "@types/node": "*" } }, + "@types/cache-manager": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.2.tgz", + "integrity": "sha512-1IwA74t5ID4KWo0Kndal16MhiPSZgMe1fGc+MLT6j5r+Ab7jku36PFTl4PP6MiWw0BJscM9QpZEo00qixNQoRg==", + "dev": true + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -13184,6 +13211,11 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -13520,6 +13552,16 @@ "unset-value": "^1.0.0" } }, + "cache-manager": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.4.tgz", + "integrity": "sha512-oayy7ukJqNlRUYNUfQBwGOLilL0X5q7GpuaF19Yqwo6qdx49OoTZKRIF5qbbr+Ru8mlTvOpvnMvVq6vw72pOPg==", + "requires": { + "async": "3.2.0", + "lodash": "^4.17.21", + "lru-cache": "6.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -16546,7 +16588,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -19518,8 +19559,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 0ef9c8f..757448e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@nestjs/swagger": "^4.7.12", "ajv": "^7.0.3", "ajv-formats": "1.5.1", + "cache-manager": "^3.4.4", "class-transformer": "^0.4.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -38,6 +39,7 @@ "@nestjs/cli": "^7.5.1", "@nestjs/schematics": "^7.1.3", "@nestjs/testing": "^7.5.1", + "@types/cache-manager": "^3.4.2", "@types/express": "^4.17.8", "@types/jest": "^26.0.15", "@types/node": "^14.14.6", diff --git a/src/app.dto.ts b/src/app.dto.ts index 6225475..693365f 100644 --- a/src/app.dto.ts +++ b/src/app.dto.ts @@ -4,3 +4,15 @@ export class AddItemDto { @ApiProperty({ description: 'Any value or object you want to add'}) data: any } + +export class AddCacheItemDto { + @ApiProperty({ description: 'Key of value'}) + key: string; + + @ApiProperty({ description: 'Any value of cache'}) + value: any; +} + + +//TODO change returning types +export class CacheItemResponse extends AddCacheItemDto {} diff --git a/src/app.module.ts b/src/app.module.ts index 80ce988..3d5bfcf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { getConfig } from './config'; import { LifoModule } from './modules/lifo/lifo.module'; +import { TtlModule } from './modules/ttl/ttl.module'; @Module({ imports: [ @@ -11,6 +12,7 @@ import { LifoModule } from './modules/lifo/lifo.module'; load: [getConfig], }), LifoModule, + TtlModule, ], providers: [ConfigService], }) diff --git a/src/common/schemas/add-ttl-item.json b/src/common/schemas/add-ttl-item.json new file mode 100644 index 0000000..7d22481 --- /dev/null +++ b/src/common/schemas/add-ttl-item.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://lifo-ttl/schemas/add-ttl-item-schema.json", + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": ["object", "boolean", "string", "number", "array", "null"] + } + }, + "required": ["key", "value"], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/common/schemas/index.ts b/src/common/schemas/index.ts index 147971a..da72c99 100644 --- a/src/common/schemas/index.ts +++ b/src/common/schemas/index.ts @@ -1,3 +1,4 @@ import * as addItemSchema from './add-item-schema.json'; +import * as addTtlItemSchema from './add-ttl-item.json'; -export { addItemSchema } \ No newline at end of file +export { addItemSchema, addTtlItemSchema } \ No newline at end of file diff --git a/src/modules/lifo/lifo.controller.ts b/src/modules/lifo/lifo.controller.ts index 4cee128..b19b93f 100644 --- a/src/modules/lifo/lifo.controller.ts +++ b/src/modules/lifo/lifo.controller.ts @@ -11,8 +11,8 @@ import { LifoService } from './lifo.service'; import { AjvValidationPipe } from '../../common/validation/AjvValidationPipe'; import { addItemSchema } from '../../common/schemas'; -@ApiTags('ttl-lifo/lifo') -@Controller('lifo') +@ApiTags('lifo') +@Controller('ttl-lifo/lifo') export class LifoController { constructor(private readonly lifoService: LifoService) {} diff --git a/src/modules/ttl/ttl.controller.ts b/src/modules/ttl/ttl.controller.ts new file mode 100644 index 0000000..a445d08 --- /dev/null +++ b/src/modules/ttl/ttl.controller.ts @@ -0,0 +1,61 @@ +import { Body, Controller, Post, Get, Delete, Param } from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiCreatedResponse, + ApiNotFoundResponse, + ApiConflictResponse, + ApiNoContentResponse, + ApiOkResponse, + ApiOperation, + ApiTags, + ApiParam, +} from '@nestjs/swagger'; +import { TtlService } from './ttl.service'; +import { CacheItemResponse, AddCacheItemDto } from '../../app.dto'; +import { AjvValidationPipe } from '../../common/validation/AjvValidationPipe'; +import { addTtlItemSchema } from '../../common/schemas'; + +@ApiTags('ttl') +@Controller('ttl-lifo/ttl') +export class TtlController { + constructor(private readonly ttlService: TtlService) {} + + @Post() + @ApiOperation({ summary: 'Add value' }) + @ApiCreatedResponse({ + description: 'Value successfully added', + type: CacheItemResponse + }) + @ApiBadRequestResponse({ description: 'Request failed with status code 400' }) + @ApiConflictResponse({ description: 'Conflict exception. Key already exists' }) + add(@Body(new AjvValidationPipe(addTtlItemSchema)) dto: AddCacheItemDto): Promise { + return this.ttlService.add(dto); + } + + @Get('/:key') + @ApiOperation({ summary: 'Get value by key' }) + @ApiParam({ name: 'key', schema: { type: 'string' } }) + @ApiOkResponse({ + description: 'Return value', + type: CacheItemResponse, + }) + @ApiNotFoundResponse({ + description: 'Not found key' + }) + get( + @Param('key') dto: string, + ): Promise { + return this.ttlService.get(dto); + } + + @Delete('/:key') + @ApiParam({ name: 'key', schema: { type: 'string' } }) + @ApiOperation({ summary: 'Remove item by key' }) + @ApiNotFoundResponse({ + description: 'Not found key' + }) + @ApiNoContentResponse({ description: 'Item successfully deleted' }) + delete(@Param('key') dto: string): Promise { + return this.ttlService.remove(dto) + } +} \ No newline at end of file diff --git a/src/modules/ttl/ttl.messages.ts b/src/modules/ttl/ttl.messages.ts new file mode 100644 index 0000000..9835cb4 --- /dev/null +++ b/src/modules/ttl/ttl.messages.ts @@ -0,0 +1,4 @@ +export const messages = { + KEY_ALREADY_EXIST: 'Key already exist', + NOT_FOUND: 'Key not found or already expired', +} \ No newline at end of file diff --git a/src/modules/ttl/ttl.module.ts b/src/modules/ttl/ttl.module.ts new file mode 100644 index 0000000..83fe660 --- /dev/null +++ b/src/modules/ttl/ttl.module.ts @@ -0,0 +1,12 @@ +import { Module, CacheModule } from '@nestjs/common'; +import { TtlController } from './ttl.controller'; +import { TtlService } from './ttl.service'; +import { TtlRepository } from './ttl.repository'; +import { LoggerService } from '../../shared/logger'; + +@Module({ + imports: [CacheModule.register()], + providers: [TtlService, TtlRepository, LoggerService], + controllers: [TtlController], +}) +export class TtlModule {} diff --git a/src/modules/ttl/ttl.repository.ts b/src/modules/ttl/ttl.repository.ts new file mode 100644 index 0000000..bef5424 --- /dev/null +++ b/src/modules/ttl/ttl.repository.ts @@ -0,0 +1,61 @@ +import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Cache } from 'cache-manager'; +import { LoggerService } from '../../shared/logger'; +import { AddCacheItemDto, CacheItemResponse } from '../../app.dto'; + +@Injectable() +export class TtlRepository { + constructor( + @Inject(CACHE_MANAGER) + private readonly cacheManager: Cache, + private readonly configService: ConfigService, + private readonly logger: LoggerService, + ) {} + + private readonly config = { + ttl: Number(this.configService.get('TTL_EXPIRATION')), + } + + async add(dto: AddCacheItemDto): Promise { + try { + return await this.cacheManager.set(dto.key, dto.value, { ttl: this.config.ttl }); + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].add`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } + + async get(key: string): Promise { + try { + return await this.cacheManager.get(key); + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].get`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } + + async remove(key: string) { + try { + return await this.cacheManager.del(key); + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].remove`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } +} diff --git a/src/modules/ttl/ttl.service.ts b/src/modules/ttl/ttl.service.ts new file mode 100644 index 0000000..fbdf36a --- /dev/null +++ b/src/modules/ttl/ttl.service.ts @@ -0,0 +1,70 @@ +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { TtlRepository } from './ttl.repository'; +import { messages } from './ttl.messages'; +import { LoggerService } from '../../shared/logger'; +import { AddCacheItemDto, CacheItemResponse } from '../../app.dto'; + +@Injectable() +export class TtlService { + constructor( + private readonly ttlRepository: TtlRepository, + private readonly logger: LoggerService, + ) {} + + async add(dto: AddCacheItemDto): Promise { + try { + const data = await this.ttlRepository.get(dto.key); + if (data) { + throw new ConflictException(messages.KEY_ALREADY_EXIST); + } + + return await this.ttlRepository.add(dto); + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].add`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } + + async get(key: string): Promise { + try { + const data = await this.ttlRepository.get(key); + if (!data) { + throw new NotFoundException(messages.NOT_FOUND); + } + + return data; + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].get`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } + + async remove(key: string): Promise { + try { + const data = await this.ttlRepository.get(key); + if (!data) { + throw new NotFoundException(messages.NOT_FOUND); + } + + return this.ttlRepository.remove(key); + } catch (e) { + this.logger.error({ + placement: `[${this.constructor.name}].remove`, + error: e, + arguments: Array.from(arguments), + }); + + throw e; + } + } +} diff --git a/src/shared/logger/index.ts b/src/shared/logger/index.ts new file mode 100644 index 0000000..6820e22 --- /dev/null +++ b/src/shared/logger/index.ts @@ -0,0 +1 @@ +export * from './logger.service'; diff --git a/src/shared/logger/logger.interfaces.ts b/src/shared/logger/logger.interfaces.ts new file mode 100644 index 0000000..6c663f2 --- /dev/null +++ b/src/shared/logger/logger.interfaces.ts @@ -0,0 +1,9 @@ +export type LoggerMessage = string | { + placement: string; + arguments?: any; + error?: Error | string; +}; + +export interface ILoggerError { + error(message: LoggerMessage): void; +} \ No newline at end of file diff --git a/src/shared/logger/logger.service.ts b/src/shared/logger/logger.service.ts new file mode 100644 index 0000000..b8c2482 --- /dev/null +++ b/src/shared/logger/logger.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { ILoggerError, LoggerMessage } from './logger.interfaces'; + +@Injectable() +export class LoggerService implements ILoggerError { + error(message: LoggerMessage) { + console.log(message); + } +} -- GitLab