diff --git a/package-lock.json b/package-lock.json index 6013a2930dbb0334526745b66e1c8e07828dd7f3..4413fc11ae0e58bf062eef1f6353b7ad5f7956b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "morgan": "^1.10.0", + "node-ttl": "^0.2.0", "reflect-metadata": "^0.1.13", "typedi": "^0.10.0" }, @@ -268,6 +269,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "node_modules/async": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", + "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1333,6 +1339,14 @@ "node": ">= 0.6" } }, + "node_modules/node-ttl": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-ttl/-/node-ttl-0.2.0.tgz", + "integrity": "sha512-bZVpmzWt4IuA3DWTZJEElLMQkYPmjUBi3q4XdBZ1dy1QLsDWLU7GMNhr/loLDFhqwlbQftQsb4xrPyCkv8UUyQ==", + "dependencies": { + "async": "*" + } + }, "node_modules/nodemon": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.13.tgz", @@ -2326,6 +2340,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "async": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.1.tgz", + "integrity": "sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3141,6 +3160,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-ttl": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-ttl/-/node-ttl-0.2.0.tgz", + "integrity": "sha512-bZVpmzWt4IuA3DWTZJEElLMQkYPmjUBi3q4XdBZ1dy1QLsDWLU7GMNhr/loLDFhqwlbQftQsb4xrPyCkv8UUyQ==", + "requires": { + "async": "*" + } + }, "nodemon": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.13.tgz", diff --git a/package.json b/package.json index b701f6a75fd7229d41ee383718fda4ac494171c3..e68f09a9a54d1daff2557b289f2eab2138423e44 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "morgan": "^1.10.0", + "node-ttl": "^0.2.0", "reflect-metadata": "^0.1.13", "typedi": "^0.10.0" }, diff --git a/src/main.ts b/src/main.ts index eaaa383c75efe3f4aa433a763de206e9397940f4..30b18912fc213cf4d4b8dc7db8bc027cb46e9bb0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ const cors = require("cors"); const morgan = require("morgan"); import { getConfig } from './config'; import { stackRouter } from './modules/stack/stack.router'; +import { ttlRouter } from './modules/ttl/ttl.router'; const app = express(); @@ -15,6 +16,7 @@ async function start() { app.use(morgan('combined')); app.use('/stack', stackRouter); + app.use('/ttl', ttlRouter); app.listen(config.PORT, () => { console.log(`Application started on port ${config.PORT}!`); diff --git a/src/modules/stack/stack.service.ts b/src/modules/stack/stack.service.ts index bb9be4b360073fb9392e5416e5202265a4ae27e9..f0bba6d72a8e7493c809f9faa9614395f7789806 100644 --- a/src/modules/stack/stack.service.ts +++ b/src/modules/stack/stack.service.ts @@ -1,6 +1,6 @@ +import { Service } from 'typedi'; import { StackDto, StackResponse } from './stack.dto'; import { StackRepository } from './stack.repository'; -import { Service } from 'typedi'; @Service() export class StackService { diff --git a/src/modules/stack/validation/stack.validation-service.ts b/src/modules/stack/validation/stack.validation-service.ts index b8d4b40c0a8b8c19d2e2e0feeaebeed87a8c5f8d..a841594cd19a1cee32c54546d944f6d613034857 100644 --- a/src/modules/stack/validation/stack.validation-service.ts +++ b/src/modules/stack/validation/stack.validation-service.ts @@ -8,7 +8,7 @@ import { StackDto } from '../stack.dto'; export class StackValidationService implements IValidateBody { async validateBody(body: StackDto) { const item = plainToClass(StackDto, body); - const errors = await validate(item); + const errors = await validate(item, { forbidNonWhitelisted: true, whitelist: true }); return errors; } diff --git a/src/modules/ttl/ttl.controller.ts b/src/modules/ttl/ttl.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..300ae4ff010bf4324990e035985ed4b41e5d2ce0 --- /dev/null +++ b/src/modules/ttl/ttl.controller.ts @@ -0,0 +1,37 @@ +import { Request, Response } from 'express'; +import { Service } from 'typedi'; +import { TtlService } from './ttl.service'; +import { TtlResponse } from './ttl.dto'; +import { TtlValidationService } from './validation'; + +@Service() +export class TtlController { + constructor( + private readonly ttlService: TtlService, + private readonly validationService: TtlValidationService, + ) {} + + async add(req: Request, res: Response): Promise> { + const errors = await this.validationService.validateBody(req.body); + if (errors.length > 0) { + return res.status(400).send({ errors }); + } + const data = this.ttlService.get(req.body.key); + if (data) { + return res.status(409).json({ message: 'Key already exist' }); + } + + const result = this.ttlService.add(req.body); + + return res.status(201).json(result); + } + + get(req: Request, res: Response): Response { + const result = this.ttlService.get(req.params.key); + if (!result) { + return res.status(404).json({ message: 'Not found key' }) + } + + return res.status(200).json(result); + } +} diff --git a/src/modules/ttl/ttl.dto.ts b/src/modules/ttl/ttl.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..938cda7cd6b789f6bd60db2551241091879d1c15 --- /dev/null +++ b/src/modules/ttl/ttl.dto.ts @@ -0,0 +1,13 @@ +import { IsString, IsNotEmpty } from 'class-validator'; + +export class TtlDto { + @IsNotEmpty() + @IsString() + key: string; + + @IsNotEmpty() + value: any; +} + + +export class TtlResponse extends TtlDto {} diff --git a/src/modules/ttl/ttl.interfaces.ts b/src/modules/ttl/ttl.interfaces.ts new file mode 100644 index 0000000000000000000000000000000000000000..38a706399e72ac3da0d903069ffd1ed1d2e0ead6 --- /dev/null +++ b/src/modules/ttl/ttl.interfaces.ts @@ -0,0 +1,5 @@ +import { TtlDto } from './ttl.dto'; + +export interface IValidateBody { + validateBody(body: TtlDto); +} \ No newline at end of file diff --git a/src/modules/ttl/ttl.repository.ts b/src/modules/ttl/ttl.repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..646361190a46df3ef96ea2396dfb996da4e7e6c3 --- /dev/null +++ b/src/modules/ttl/ttl.repository.ts @@ -0,0 +1,28 @@ +const NodeTtl = require('node-ttl'); +import { Service } from 'typedi'; +import { TtlDto, TtlResponse } from './ttl.dto'; +import { getConfig } from '../../config'; + +@Service() +export class TtlRepository { + private readonly store; + private readonly config; + constructor() { + this.store = new NodeTtl(); + this.config = getConfig(); + } + + add(dto: TtlDto): TtlResponse { + this.store.push(dto.key, dto.value, null, this.config.TTL_EXPIRATION); + + return dto; + } + + get(key: string): TtlResponse { + const value = this.store.get(key); + if (!value) { + return value; + } + return { key, value } + } +} diff --git a/src/modules/ttl/ttl.router.ts b/src/modules/ttl/ttl.router.ts new file mode 100644 index 0000000000000000000000000000000000000000..935a0ee08d318de84530eda6d737e0d15312d9c6 --- /dev/null +++ b/src/modules/ttl/ttl.router.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { Container } from 'typedi'; +import { TtlController } from './ttl.controller'; + +const ttlRouter = Router(); +const ttlController = Container.get(TtlController); + +ttlRouter.get( + '/:key', + ttlController.get.bind(ttlController), +); + +ttlRouter.post( + '/', + ttlController.add.bind(ttlController), +); + +export { ttlRouter } \ No newline at end of file diff --git a/src/modules/ttl/ttl.service.ts b/src/modules/ttl/ttl.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..292fdc01e21b544a823192a5de027957ff9a4658 --- /dev/null +++ b/src/modules/ttl/ttl.service.ts @@ -0,0 +1,16 @@ +import { Service } from 'typedi'; +import { TtlRepository } from './ttl.repository'; +import { TtlDto, TtlResponse } from './ttl.dto'; + +@Service() +export class TtlService { + constructor(private readonly ttlRepository: TtlRepository) {} + + add(dto: TtlDto): TtlResponse { + return this.ttlRepository.add(dto); + } + + get(key: string): TtlResponse { + return this.ttlRepository.get(key); + } +} \ No newline at end of file diff --git a/src/modules/ttl/validation/index.ts b/src/modules/ttl/validation/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d7e552984c3e942a00398eb33908a088c4e03e5 --- /dev/null +++ b/src/modules/ttl/validation/index.ts @@ -0,0 +1 @@ +export * from './ttl.validation-service'; diff --git a/src/modules/ttl/validation/ttl.validation-service.ts b/src/modules/ttl/validation/ttl.validation-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a5a9d3eb625b17c21da67396c96f9e65e708e63 --- /dev/null +++ b/src/modules/ttl/validation/ttl.validation-service.ts @@ -0,0 +1,16 @@ +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; +import { Service } from 'typedi'; +import { IValidateBody } from '../ttl.interfaces'; +import { TtlDto } from '../ttl.dto'; + +@Service() +export class TtlValidationService implements IValidateBody { + async validateBody(body: TtlDto) { + const item = plainToClass(TtlDto, body); + const errors = await validate(item, { forbidNonWhitelisted: true, whitelist: true }); + + return errors; + } + +} \ No newline at end of file