├── .env.example ├── .prettierrc ├── src ├── helpers │ └── Replace.ts ├── application │ ├── use-cases │ │ ├── errors │ │ │ └── notification-not-found.ts │ │ ├── send-notification.spec.ts │ │ ├── count-recipient-notifications.ts │ │ ├── get-recipient-notifications.ts │ │ ├── read-notification.ts │ │ ├── cancel-notification.ts │ │ ├── unread-notification.ts │ │ ├── send-notification.ts │ │ ├── count-recipient-notifications.spec.ts │ │ ├── read-notification.spec.ts │ │ ├── cancel-notification.spec.ts │ │ ├── unread-notification.spec.ts │ │ └── get-recipient-notifications.spec.ts │ ├── entities │ │ ├── notification.spec.ts │ │ ├── content.ts │ │ ├── content.spec.ts │ │ └── notification.ts │ └── repositories │ │ └── notifications-repository.ts ├── infra │ ├── http │ │ ├── dtos │ │ │ └── create-notification-body.ts │ │ ├── view-models │ │ │ └── notification-view-model.ts │ │ ├── http.module.ts │ │ └── controllers │ │ │ └── notifications.controller.ts │ ├── database │ │ ├── prisma │ │ │ ├── prisma.service.ts │ │ │ ├── mappers │ │ │ │ └── prisma-notification-mapper.ts │ │ │ └── repositories │ │ │ │ └── prisma-notifications-repository.ts │ │ └── database.module.ts │ └── messaging │ │ ├── messaging.module.ts │ │ └── kafka │ │ ├── controllers │ │ └── notifications.controller.ts │ │ └── kafka-consumer.service.ts ├── app.module.ts └── main.ts ├── prisma ├── dev.db ├── migrations │ ├── 20221209142550_add_cancel_at_on_notifications │ │ └── migration.sql │ ├── migration_lock.toml │ └── 20221207174028_create_notifications │ │ └── migration.sql └── schema.prisma ├── tsconfig.build.json ├── nest-cli.json ├── test ├── jest-e2e.json ├── factories │ └── notification-factory.ts └── repositories │ └── in-memory-notifications-repository.ts ├── .gitignore ├── jest.config.ts ├── .eslintrc.js ├── tsconfig.json ├── README.md ├── package.json └── Insomnia.json /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./dev.db" 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/helpers/Replace.ts: -------------------------------------------------------------------------------- 1 | export type Replace = Omit & R; 2 | -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/ignite-lab-nodejs/HEAD/prisma/dev.db -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /prisma/migrations/20221209142550_add_cancel_at_on_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Notification" ADD COLUMN "canceledAt" DATETIME; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /src/application/use-cases/errors/notification-not-found.ts: -------------------------------------------------------------------------------- 1 | export class NotificationNotFound extends Error { 2 | constructor() { 3 | super('Notification not found.'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/infra/http/dtos/create-notification-body.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsUUID, Length } from 'class-validator'; 2 | 3 | export class CreateNotificationBody { 4 | @IsNotEmpty() 5 | @IsUUID() 6 | recipientId: string; 7 | 8 | @IsNotEmpty() 9 | @Length(5, 240) 10 | content: string; 11 | 12 | @IsNotEmpty() 13 | category: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DatabaseModule } from './infra/database/database.module'; 4 | import { HttpModule } from './infra/http/http.module'; 5 | import { MessagingModule } from './infra/messaging/messaging.module'; 6 | 7 | @Module({ 8 | imports: [HttpModule, DatabaseModule, MessagingModule], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /src/infra/http/view-models/notification-view-model.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '@application/entities/notification'; 2 | 3 | export class NotificationViewModel { 4 | static toHTTP(notification: Notification) { 5 | return { 6 | id: notification.id, 7 | content: notification.content.value, 8 | category: notification.category, 9 | recipientId: notification.recipientId, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Notification { 11 | id String @id 12 | recipientId String 13 | content String 14 | category String 15 | readAt DateTime? 16 | canceledAt DateTime? 17 | createdAt DateTime @default(now()) 18 | 19 | @@index([recipientId]) 20 | } 21 | -------------------------------------------------------------------------------- /prisma/migrations/20221207174028_create_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Notification" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "recipientId" TEXT NOT NULL, 5 | "content" TEXT NOT NULL, 6 | "category" TEXT NOT NULL, 7 | "readAt" DATETIME, 8 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | 11 | -- CreateIndex 12 | CREATE INDEX "Notification_recipientId_idx" ON "Notification"("recipientId"); 13 | -------------------------------------------------------------------------------- /src/application/entities/notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from './notification'; 2 | import { Content } from './content'; 3 | 4 | describe('Notification', () => { 5 | it('should be able to create a notification', () => { 6 | const notification = new Notification({ 7 | content: new Content('Nova solicitação de amizade'), 8 | category: 'social', 9 | recipientId: 'example-recipient-id', 10 | }); 11 | 12 | expect(notification).toBeTruthy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/factories/notification-factory.ts: -------------------------------------------------------------------------------- 1 | import { Content } from '@application/entities/content'; 2 | import { 3 | Notification, 4 | NotificationProps, 5 | } from '@application/entities/notification'; 6 | 7 | type Override = Partial; 8 | 9 | export function makeNotification(override: Override = {}) { 10 | return new Notification({ 11 | category: 'social', 12 | content: new Content('Nova solicitação de amizade!'), 13 | recipientId: 'recipient-2', 14 | ...override, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/application/repositories/notifications-repository.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '../entities/notification'; 2 | 3 | export abstract class NotificationsRepository { 4 | abstract create(notification: Notification): Promise; 5 | abstract findById(notificationId: string): Promise; 6 | abstract save(notification: Notification): Promise; 7 | abstract countManyByRecipientId(recipientId: string): Promise; 8 | abstract findManyByRecipientId(recipientId: string): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env 38 | -------------------------------------------------------------------------------- /src/application/entities/content.ts: -------------------------------------------------------------------------------- 1 | export class Content { 2 | private readonly content: string; 3 | 4 | get value(): string { 5 | return this.content; 6 | } 7 | 8 | private validateContentLength(content: string): boolean { 9 | return content.length > 5 && content.length <= 240; 10 | } 11 | 12 | constructor(content: string) { 13 | const isContentLengthValid = this.validateContentLength(content); 14 | 15 | if (!isContentLengthValid) { 16 | throw new Error('Content length error.'); 17 | } 18 | 19 | this.content = content; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/database/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService extends PrismaClient implements OnModuleInit { 6 | constructor() { 7 | super({ 8 | log: ['query'], 9 | }); 10 | } 11 | 12 | async onModuleInit() { 13 | await this.$connect(); 14 | } 15 | 16 | async enableShutdownHooks(app: INestApplication) { 17 | this.$on('beforeExit', async () => { 18 | await app.close(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infra/messaging/messaging.module.ts: -------------------------------------------------------------------------------- 1 | import { SendNotification } from '@application/use-cases/send-notification'; 2 | import { DatabaseModule } from '@infra/database/database.module'; 3 | import { Module } from '@nestjs/common'; 4 | import { NotificationsController } from './kafka/controllers/notifications.controller'; 5 | import { KafkaConsumerService } from './kafka/kafka-consumer.service'; 6 | 7 | @Module({ 8 | imports: [DatabaseModule], 9 | providers: [KafkaConsumerService, SendNotification], 10 | controllers: [NotificationsController], 11 | }) 12 | export class MessagingModule {} 13 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'jest'; 2 | import { pathsToModuleNameMapper } from 'ts-jest'; 3 | import { compilerOptions } from './tsconfig.json'; 4 | 5 | const config: Config = { 6 | moduleFileExtensions: ['js', 'json', 'ts'], 7 | testRegex: '.*\\.spec\\.ts$', 8 | transform: { 9 | '^.+\\.(t|j)s$': 'ts-jest', 10 | }, 11 | collectCoverageFrom: ['**/*.(t|j)s'], 12 | coverageDirectory: '../coverage', 13 | testEnvironment: 'node', 14 | 15 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 16 | prefix: '/', 17 | }), 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/infra/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NotificationsRepository } from '@application/repositories/notifications-repository'; 3 | import { PrismaNotificationsRepository } from '@infra/database/prisma/repositories/prisma-notifications-repository'; 4 | 5 | import { PrismaService } from './prisma/prisma.service'; 6 | 7 | @Module({ 8 | providers: [ 9 | PrismaService, 10 | { 11 | provide: NotificationsRepository, 12 | useClass: PrismaNotificationsRepository, 13 | }, 14 | ], 15 | exports: [NotificationsRepository], 16 | }) 17 | export class DatabaseModule {} 18 | -------------------------------------------------------------------------------- /src/application/entities/content.spec.ts: -------------------------------------------------------------------------------- 1 | import { Content } from './content'; 2 | 3 | describe('Notification Content', () => { 4 | it('should be able to create a notification content', () => { 5 | const content = new Content('Você recebeu uma solicitação de amizade'); 6 | 7 | expect(content).toBeTruthy(); 8 | }); 9 | 10 | it('should not be able to create a notification content with less than 5 characters', () => { 11 | expect(() => new Content('aaa')).toThrow(); 12 | }); 13 | 14 | it('should not be able to create a notification content with more than 240 characters', () => { 15 | expect(() => new Content('a'.repeat(241))).toThrow(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { KafkaConsumerService } from '@infra/messaging/kafka/kafka-consumer.service'; 2 | import { ValidationPipe } from '@nestjs/common'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { MicroserviceOptions } from '@nestjs/microservices'; 5 | import { AppModule } from './app.module'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | 10 | app.useGlobalPipes(new ValidationPipe()); 11 | 12 | const kafkaConsumerService = app.get(KafkaConsumerService); 13 | 14 | app.connectMicroservice({ 15 | strategy: kafkaConsumerService, 16 | }); 17 | 18 | await app.startAllMicroservices(); 19 | await app.listen(3000); 20 | } 21 | 22 | bootstrap(); 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/infra/messaging/kafka/controllers/notifications.controller.ts: -------------------------------------------------------------------------------- 1 | import { SendNotification } from '@application/use-cases/send-notification'; 2 | import { Controller } from '@nestjs/common'; 3 | import { EventPattern, Payload } from '@nestjs/microservices'; 4 | 5 | interface SendNotificationPayload { 6 | content: string; 7 | category: string; 8 | recipientId: string; 9 | } 10 | 11 | @Controller() 12 | export class NotificationsController { 13 | constructor(private sendNotification: SendNotification) {} 14 | 15 | @EventPattern('notifications.send-notification') 16 | async handleSendNotification( 17 | @Payload() { content, category, recipientId }: SendNotificationPayload, 18 | ) { 19 | await this.sendNotification.execute({ 20 | content, 21 | category, 22 | recipientId, 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/application/use-cases/send-notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 2 | import { SendNotification } from './send-notification'; 3 | 4 | describe('Send notification', () => { 5 | it('should be able to send a notification', async () => { 6 | const notificationsRepository = new InMemoryNotificationsRepository(); 7 | const sendNotification = new SendNotification(notificationsRepository); 8 | 9 | const { notification } = await sendNotification.execute({ 10 | category: 'social', 11 | content: 'THis is a notification', 12 | recipientId: 'example-recipient-id', 13 | }); 14 | 15 | expect(notificationsRepository.notifications).toHaveLength(1); 16 | expect(notificationsRepository.notifications[0]).toEqual(notification); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/application/use-cases/count-recipient-notifications.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { NotificationsRepository } from '../repositories/notifications-repository'; 3 | 4 | interface CountRecipientNotificationsRequest { 5 | recipientId: string; 6 | } 7 | 8 | interface CountRecipientNotificationsResponse { 9 | count: number; 10 | } 11 | 12 | @Injectable() 13 | export class CountRecipientNotifications { 14 | constructor(private notificationsRepository: NotificationsRepository) {} 15 | 16 | async execute( 17 | request: CountRecipientNotificationsRequest, 18 | ): Promise { 19 | const { recipientId } = request; 20 | 21 | const count = await this.notificationsRepository.countManyByRecipientId( 22 | recipientId, 23 | ); 24 | 25 | return { 26 | count, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/infra/messaging/kafka/kafka-consumer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy } from '@nestjs/common'; 2 | import { ServerKafka } from '@nestjs/microservices'; 3 | 4 | @Injectable() 5 | export class KafkaConsumerService 6 | extends ServerKafka 7 | implements OnModuleDestroy 8 | { 9 | constructor() { 10 | super({ 11 | client: { 12 | clientId: 'notifications', 13 | brokers: ['stirred-bream-9626-us1-kafka.upstash.io:9092'], 14 | sasl: { 15 | mechanism: 'scram-sha-256', 16 | username: 17 | 'c3RpcnJlZC1icmVhbS05NjI2JP2jGZobUfgOvOJRt4BKE7U2ACdlOBKC3bMvgEk', 18 | password: 19 | 'H4nly5QgkjLn2n3y2_9CLRKLdWPUw8ttXPwqBrladJfdawcBIk0PPMWLrcofbp3-i2Ynhg==', 20 | }, 21 | ssl: true, 22 | }, 23 | }); 24 | } 25 | 26 | async onModuleDestroy() { 27 | await this.close(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "resolveJsonModule": true, 18 | "strictPropertyInitialization": false, 19 | "noImplicitAny": false, 20 | "strictBindCallApply": false, 21 | "forceConsistentCasingInFileNames": false, 22 | "noFallthroughCasesInSwitch": false, 23 | "paths": { 24 | "@application/*":["./src/application/*"], 25 | "@helpers/*":["./src/helpers/*"], 26 | "@infra/*":["./src/infra/*"], 27 | "@test/*":["./test/*"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/application/use-cases/get-recipient-notifications.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '@application/entities/notification'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { NotificationsRepository } from '../repositories/notifications-repository'; 4 | 5 | interface GetRecipientNotificationsRequest { 6 | recipientId: string; 7 | } 8 | 9 | interface GetRecipientNotificationsResponse { 10 | notifications: Notification[]; 11 | } 12 | 13 | @Injectable() 14 | export class GetRecipientNotifications { 15 | constructor(private notificationsRepository: NotificationsRepository) {} 16 | 17 | async execute( 18 | request: GetRecipientNotificationsRequest, 19 | ): Promise { 20 | const { recipientId } = request; 21 | 22 | const notifications = 23 | await this.notificationsRepository.findManyByRecipientId(recipientId); 24 | 25 | return { 26 | notifications, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/application/use-cases/read-notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { NotificationsRepository } from '../repositories/notifications-repository'; 4 | 5 | interface ReadNotificationRequest { 6 | notificationId: string; 7 | } 8 | 9 | type ReadNotificationResponse = void; 10 | 11 | @Injectable() 12 | export class ReadNotification { 13 | constructor(private notificationsRepository: NotificationsRepository) {} 14 | 15 | async execute( 16 | request: ReadNotificationRequest, 17 | ): Promise { 18 | const { notificationId } = request; 19 | 20 | const notification = await this.notificationsRepository.findById( 21 | notificationId, 22 | ); 23 | 24 | if (!notification) { 25 | throw new NotificationNotFound(); 26 | } 27 | 28 | notification.read(); 29 | 30 | await this.notificationsRepository.save(notification); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/application/use-cases/cancel-notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { NotificationsRepository } from '../repositories/notifications-repository'; 4 | 5 | interface CancelNotificationRequest { 6 | notificationId: string; 7 | } 8 | 9 | type CancelNotificationResponse = void; 10 | 11 | @Injectable() 12 | export class CancelNotification { 13 | constructor(private notificationsRepository: NotificationsRepository) {} 14 | 15 | async execute( 16 | request: CancelNotificationRequest, 17 | ): Promise { 18 | const { notificationId } = request; 19 | 20 | const notification = await this.notificationsRepository.findById( 21 | notificationId, 22 | ); 23 | 24 | if (!notification) { 25 | throw new NotificationNotFound(); 26 | } 27 | 28 | notification.cancel(); 29 | 30 | await this.notificationsRepository.save(notification); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/application/use-cases/unread-notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { NotificationsRepository } from '../repositories/notifications-repository'; 4 | 5 | interface UnreadNotificationRequest { 6 | notificationId: string; 7 | } 8 | 9 | type UnreadNotificationResponse = void; 10 | 11 | @Injectable() 12 | export class UnreadNotification { 13 | constructor(private notificationsRepository: NotificationsRepository) {} 14 | 15 | async execute( 16 | request: UnreadNotificationRequest, 17 | ): Promise { 18 | const { notificationId } = request; 19 | 20 | const notification = await this.notificationsRepository.findById( 21 | notificationId, 22 | ); 23 | 24 | if (!notification) { 25 | throw new NotificationNotFound(); 26 | } 27 | 28 | notification.unread(); 29 | 30 | await this.notificationsRepository.save(notification); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Rocketseat Education 3 |

4 | 5 |

6 | Rocketseat Project 7 | License 8 |

9 | 10 | 11 | ## 💻 Projeto 12 | 13 | ignite-lab-nodejs 14 | 15 | ## 📝 Licença 16 | 17 | Esse projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes. 18 | 19 | --- 20 | 21 |

22 | Feito com 💜 by Rocketseat 23 |

24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |

32 | 33 | banner 34 | 35 |

36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/infra/database/prisma/mappers/prisma-notification-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Notification as RawNotification } from '@prisma/client'; 2 | import { Notification } from '@application/entities/notification'; 3 | import { Content } from '@application/entities/content'; 4 | 5 | export class PrismaNotificationMapper { 6 | static toPrisma(notification: Notification) { 7 | return { 8 | id: notification.id, 9 | content: notification.content.value, 10 | category: notification.category, 11 | recipientId: notification.recipientId, 12 | readAt: notification.readAt, 13 | canceledAt: notification.canceledAt, 14 | createdAt: notification.createdAt, 15 | }; 16 | } 17 | 18 | static toDomain(raw: RawNotification): Notification { 19 | return new Notification( 20 | { 21 | category: raw.category, 22 | content: new Content(raw.content), 23 | recipientId: raw.recipientId, 24 | readAt: raw.readAt, 25 | canceledAt: raw.canceledAt, 26 | createdAt: raw.createdAt, 27 | }, 28 | raw.id, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/infra/http/http.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DatabaseModule } from '@infra/database/database.module'; 3 | 4 | import { SendNotification } from '@application/use-cases/send-notification'; 5 | import { NotificationsController } from './controllers/notifications.controller'; 6 | import { CancelNotification } from '@application/use-cases/cancel-notification'; 7 | import { CountRecipientNotifications } from '@application/use-cases/count-recipient-notifications'; 8 | import { GetRecipientNotifications } from '@application/use-cases/get-recipient-notifications'; 9 | import { ReadNotification } from '@application/use-cases/read-notification'; 10 | import { UnreadNotification } from '@application/use-cases/unread-notification'; 11 | 12 | @Module({ 13 | imports: [DatabaseModule], 14 | controllers: [NotificationsController], 15 | providers: [ 16 | SendNotification, 17 | CancelNotification, 18 | CountRecipientNotifications, 19 | GetRecipientNotifications, 20 | ReadNotification, 21 | UnreadNotification, 22 | ], 23 | }) 24 | export class HttpModule {} 25 | -------------------------------------------------------------------------------- /src/application/use-cases/send-notification.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { NotificationsRepository } from '../repositories/notifications-repository'; 3 | import { Content } from '../entities/content'; 4 | import { Notification } from '../entities/notification'; 5 | 6 | interface SendNotificationRequest { 7 | recipientId: string; 8 | content: string; 9 | category: string; 10 | } 11 | 12 | interface SendNotificationResponse { 13 | notification: Notification; 14 | } 15 | 16 | @Injectable() 17 | export class SendNotification { 18 | constructor(private notificationsRepository: NotificationsRepository) {} 19 | 20 | async execute( 21 | request: SendNotificationRequest, 22 | ): Promise { 23 | const { recipientId, content, category } = request; 24 | 25 | const notification = new Notification({ 26 | recipientId, 27 | content: new Content(content), 28 | category, 29 | }); 30 | 31 | await this.notificationsRepository.create(notification); 32 | 33 | return { 34 | notification, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/application/use-cases/count-recipient-notifications.spec.ts: -------------------------------------------------------------------------------- 1 | import { CountRecipientNotifications } from '@application/use-cases/count-recipient-notifications'; 2 | import { makeNotification } from '@test/factories/notification-factory'; 3 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 4 | 5 | describe('Count recipient notifications', () => { 6 | it('should be able to recipient notifications', async () => { 7 | const notificationsRepository = new InMemoryNotificationsRepository(); 8 | const countRecipientNotifications = new CountRecipientNotifications( 9 | notificationsRepository, 10 | ); 11 | 12 | await notificationsRepository.create( 13 | makeNotification({ 14 | recipientId: 'recipient-1', 15 | }), 16 | ); 17 | 18 | await notificationsRepository.create( 19 | makeNotification({ 20 | recipientId: 'recipient-1', 21 | }), 22 | ); 23 | 24 | await notificationsRepository.create( 25 | makeNotification({ 26 | recipientId: 'recipient-2', 27 | }), 28 | ); 29 | 30 | const { count } = await countRecipientNotifications.execute({ 31 | recipientId: 'recipient-1', 32 | }); 33 | 34 | expect(count).toBe(2); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/application/use-cases/read-notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { ReadNotification } from '@application/use-cases/read-notification'; 3 | import { makeNotification } from '@test/factories/notification-factory'; 4 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 5 | 6 | describe('Read notification', () => { 7 | it('should be able to read a notification', async () => { 8 | const notificationsRepository = new InMemoryNotificationsRepository(); 9 | const readNotification = new ReadNotification(notificationsRepository); 10 | 11 | const notification = makeNotification(); 12 | 13 | await notificationsRepository.create(notification); 14 | 15 | await readNotification.execute({ 16 | notificationId: notification.id, 17 | }); 18 | 19 | expect(notificationsRepository.notifications[0].readAt).toEqual( 20 | expect.any(Date), 21 | ); 22 | }); 23 | 24 | it('should not be able to read a non existing notification', async () => { 25 | const notificationsRepository = new InMemoryNotificationsRepository(); 26 | const readNotification = new ReadNotification(notificationsRepository); 27 | 28 | expect(() => { 29 | return readNotification.execute({ 30 | notificationId: 'fake-notification-id', 31 | }); 32 | }).rejects.toThrow(NotificationNotFound); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/application/use-cases/cancel-notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { makeNotification } from '@test/factories/notification-factory'; 3 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 4 | import { CancelNotification } from './cancel-notification'; 5 | 6 | describe('Cancel notification', () => { 7 | it('should be able to cancel a notification', async () => { 8 | const notificationsRepository = new InMemoryNotificationsRepository(); 9 | const cancelNotification = new CancelNotification(notificationsRepository); 10 | 11 | const notification = makeNotification(); 12 | 13 | await notificationsRepository.create(notification); 14 | 15 | await cancelNotification.execute({ 16 | notificationId: notification.id, 17 | }); 18 | 19 | expect(notificationsRepository.notifications[0].canceledAt).toEqual( 20 | expect.any(Date), 21 | ); 22 | }); 23 | 24 | it('should not be able to cancel a non existing notification', async () => { 25 | const notificationsRepository = new InMemoryNotificationsRepository(); 26 | const cancelNotification = new CancelNotification(notificationsRepository); 27 | 28 | expect(() => { 29 | return cancelNotification.execute({ 30 | notificationId: 'fake-notification-id', 31 | }); 32 | }).rejects.toThrow(NotificationNotFound); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/application/use-cases/unread-notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotificationNotFound } from '@application/use-cases/errors/notification-not-found'; 2 | import { UnreadNotification } from '@application/use-cases/unread-notification'; 3 | import { makeNotification } from '@test/factories/notification-factory'; 4 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 5 | 6 | describe('Unread notification', () => { 7 | it('should be able to unread a notification', async () => { 8 | const notificationsRepository = new InMemoryNotificationsRepository(); 9 | const unreadNotification = new UnreadNotification(notificationsRepository); 10 | 11 | const notification = makeNotification({ 12 | readAt: new Date(), 13 | }); 14 | 15 | await notificationsRepository.create(notification); 16 | 17 | await unreadNotification.execute({ 18 | notificationId: notification.id, 19 | }); 20 | 21 | expect(notificationsRepository.notifications[0].readAt).toBeNull(); 22 | }); 23 | 24 | it('should not be able to unread a non existing notification', async () => { 25 | const notificationsRepository = new InMemoryNotificationsRepository(); 26 | const unreadNotification = new UnreadNotification(notificationsRepository); 27 | 28 | expect(() => { 29 | return unreadNotification.execute({ 30 | notificationId: 'fake-notification-id', 31 | }); 32 | }).rejects.toThrow(NotificationNotFound); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/repositories/in-memory-notifications-repository.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '@application/entities/notification'; 2 | import { NotificationsRepository } from '@application/repositories/notifications-repository'; 3 | 4 | export class InMemoryNotificationsRepository 5 | implements NotificationsRepository 6 | { 7 | public notifications: Notification[] = []; 8 | 9 | async findById(notificationId: string): Promise { 10 | const notification = this.notifications.find( 11 | (item) => item.id === notificationId, 12 | ); 13 | 14 | if (!notification) { 15 | return null; 16 | } 17 | 18 | return notification; 19 | } 20 | 21 | async findManyByRecipientId(recipientId: string): Promise { 22 | return this.notifications.filter( 23 | (item) => item.recipientId === recipientId, 24 | ); 25 | } 26 | 27 | async countManyByRecipientId(recipientId: string): Promise { 28 | return this.notifications.filter((item) => item.recipientId === recipientId) 29 | .length; 30 | } 31 | 32 | async create(notification: Notification) { 33 | this.notifications.push(notification); 34 | } 35 | 36 | async save(notification: Notification): Promise { 37 | const notificationIndex = this.notifications.findIndex( 38 | (item) => item.id === notification.id, 39 | ); 40 | 41 | if (notificationIndex >= 0) { 42 | this.notifications[notificationIndex] = notification; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/application/use-cases/get-recipient-notifications.spec.ts: -------------------------------------------------------------------------------- 1 | import { GetRecipientNotifications } from '@application/use-cases/get-recipient-notifications'; 2 | import { makeNotification } from '@test/factories/notification-factory'; 3 | import { InMemoryNotificationsRepository } from '@test/repositories/in-memory-notifications-repository'; 4 | 5 | describe('Get recipient notifications', () => { 6 | it('should be able to recipient notifications', async () => { 7 | const notificationsRepository = new InMemoryNotificationsRepository(); 8 | const getRecipientNotifications = new GetRecipientNotifications( 9 | notificationsRepository, 10 | ); 11 | 12 | await notificationsRepository.create( 13 | makeNotification({ 14 | recipientId: 'recipient-1', 15 | }), 16 | ); 17 | 18 | await notificationsRepository.create( 19 | makeNotification({ 20 | recipientId: 'recipient-1', 21 | }), 22 | ); 23 | 24 | await notificationsRepository.create( 25 | makeNotification({ 26 | recipientId: 'recipient-2', 27 | }), 28 | ); 29 | 30 | const { notifications } = await getRecipientNotifications.execute({ 31 | recipientId: 'recipient-1', 32 | }); 33 | 34 | expect(notifications).toHaveLength(2); 35 | expect(notifications).toEqual( 36 | expect.arrayContaining([ 37 | expect.objectContaining({ recipientId: 'recipient-1' }), 38 | expect.objectContaining({ recipientId: 'recipient-1' }), 39 | ]), 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/application/entities/notification.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import { Replace } from '../../helpers/Replace'; 3 | import { Content } from './content'; 4 | 5 | export interface NotificationProps { 6 | recipientId: string; 7 | content: Content; 8 | category: string; 9 | readAt?: Date | null; 10 | canceledAt?: Date | null; 11 | createdAt: Date; 12 | } 13 | 14 | export class Notification { 15 | private _id: string; 16 | private props: NotificationProps; 17 | 18 | constructor( 19 | props: Replace, 20 | id?: string, 21 | ) { 22 | this._id = id ?? randomUUID(); 23 | this.props = { 24 | ...props, 25 | createdAt: props.createdAt ?? new Date(), 26 | }; 27 | } 28 | 29 | public get id() { 30 | return this._id; 31 | } 32 | 33 | public set recipientId(recipientId: string) { 34 | this.props.recipientId = recipientId; 35 | } 36 | 37 | public get recipientId(): string { 38 | return this.props.recipientId; 39 | } 40 | 41 | public set content(content: Content) { 42 | this.props.content = content; 43 | } 44 | 45 | public get content(): Content { 46 | return this.props.content; 47 | } 48 | 49 | public set category(category: string) { 50 | this.props.category = category; 51 | } 52 | 53 | public get category(): string { 54 | return this.props.category; 55 | } 56 | 57 | public read() { 58 | this.props.readAt = new Date(); 59 | } 60 | 61 | public unread() { 62 | this.props.readAt = null; 63 | } 64 | 65 | public get readAt(): Date | null | undefined { 66 | return this.props.readAt; 67 | } 68 | 69 | public cancel() { 70 | this.props.canceledAt = new Date(); 71 | } 72 | 73 | public get canceledAt(): Date | null | undefined { 74 | return this.props.canceledAt; 75 | } 76 | 77 | public get createdAt(): Date { 78 | return this.props.createdAt; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/infra/database/prisma/repositories/prisma-notifications-repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Notification } from '@application/entities/notification'; 3 | import { NotificationsRepository } from '@application/repositories/notifications-repository'; 4 | import { PrismaService } from '../prisma.service'; 5 | import { PrismaNotificationMapper } from '@infra/database/prisma/mappers/prisma-notification-mapper'; 6 | 7 | @Injectable() 8 | export class PrismaNotificationsRepository implements NotificationsRepository { 9 | constructor(private prisma: PrismaService) {} 10 | 11 | async findById(notificationId: string): Promise { 12 | const notification = await this.prisma.notification.findUnique({ 13 | where: { 14 | id: notificationId, 15 | }, 16 | }); 17 | 18 | if (!notification) { 19 | return null; 20 | } 21 | 22 | return PrismaNotificationMapper.toDomain(notification); 23 | } 24 | 25 | async findManyByRecipientId(recipientId: string): Promise { 26 | const notifications = await this.prisma.notification.findMany({ 27 | where: { 28 | recipientId, 29 | }, 30 | }); 31 | 32 | return notifications.map(PrismaNotificationMapper.toDomain); 33 | } 34 | 35 | async countManyByRecipientId(recipientId: string): Promise { 36 | const count = await this.prisma.notification.count({ 37 | where: { 38 | recipientId, 39 | }, 40 | }); 41 | 42 | return count; 43 | } 44 | 45 | async create(notification: Notification): Promise { 46 | const raw = PrismaNotificationMapper.toPrisma(notification); 47 | 48 | await this.prisma.notification.create({ 49 | data: raw, 50 | }); 51 | } 52 | 53 | async save(notification: Notification): Promise { 54 | const raw = PrismaNotificationMapper.toPrisma(notification); 55 | 56 | await this.prisma.notification.update({ 57 | where: { 58 | id: raw.id, 59 | }, 60 | data: raw, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notifications-service", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/core": "^9.0.0", 26 | "@nestjs/microservices": "^9.2.1", 27 | "@nestjs/platform-express": "^9.0.0", 28 | "@prisma/client": "^4.7.1", 29 | "class-transformer": "^0.5.1", 30 | "class-validator": "^0.13.2", 31 | "kafkajs": "^2.2.3", 32 | "reflect-metadata": "^0.1.13", 33 | "rimraf": "^3.0.2", 34 | "rxjs": "^7.2.0" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^9.0.0", 38 | "@nestjs/schematics": "^9.0.0", 39 | "@nestjs/testing": "^9.0.0", 40 | "@types/express": "^4.17.13", 41 | "@types/jest": "28.1.8", 42 | "@types/node": "^16.0.0", 43 | "@types/supertest": "^2.0.11", 44 | "@typescript-eslint/eslint-plugin": "^5.0.0", 45 | "@typescript-eslint/parser": "^5.0.0", 46 | "eslint": "^8.0.1", 47 | "eslint-config-prettier": "^8.3.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "jest": "28.1.3", 50 | "prettier": "^2.3.2", 51 | "prisma": "^4.7.1", 52 | "source-map-support": "^0.5.20", 53 | "supertest": "^6.1.3", 54 | "ts-jest": "28.0.8", 55 | "ts-loader": "^9.2.3", 56 | "ts-node": "^10.0.0", 57 | "tsconfig-paths": "4.1.0", 58 | "typescript": "^4.7.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/infra/http/controllers/notifications.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; 2 | import { SendNotification } from '@application/use-cases/send-notification'; 3 | import { CreateNotificationBody } from '../dtos/create-notification-body'; 4 | import { NotificationViewModel } from '@infra/http/view-models/notification-view-model'; 5 | import { CancelNotification } from '@application/use-cases/cancel-notification'; 6 | import { ReadNotification } from '@application/use-cases/read-notification'; 7 | import { UnreadNotification } from '@application/use-cases/unread-notification'; 8 | import { CountRecipientNotifications } from '@application/use-cases/count-recipient-notifications'; 9 | import { GetRecipientNotifications } from '@application/use-cases/get-recipient-notifications'; 10 | 11 | @Controller('notifications') 12 | export class NotificationsController { 13 | constructor( 14 | private sendNotification: SendNotification, 15 | private cancelNotification: CancelNotification, 16 | private readNotification: ReadNotification, 17 | private unreadNotification: UnreadNotification, 18 | private countRecipientNotifications: CountRecipientNotifications, 19 | private getRecipientNotifications: GetRecipientNotifications, 20 | ) {} 21 | 22 | @Patch(':id/cancel') 23 | async cancel(@Param('id') id: string) { 24 | await this.cancelNotification.execute({ 25 | notificationId: id, 26 | }); 27 | } 28 | 29 | @Get('count/from/:recipientId') 30 | async countFromRecipient(@Param('recipientId') recipientId: string) { 31 | const { count } = await this.countRecipientNotifications.execute({ 32 | recipientId, 33 | }); 34 | 35 | return { 36 | count, 37 | }; 38 | } 39 | 40 | @Get('from/:recipientId') 41 | async getFromRecipient(@Param('recipientId') recipientId: string) { 42 | const { notifications } = await this.getRecipientNotifications.execute({ 43 | recipientId, 44 | }); 45 | 46 | return { 47 | notifications: notifications.map(NotificationViewModel.toHTTP), 48 | }; 49 | } 50 | 51 | @Patch(':id/read') 52 | async read(@Param('id') id: string) { 53 | await this.readNotification.execute({ 54 | notificationId: id, 55 | }); 56 | } 57 | 58 | @Patch(':id/unread') 59 | async unread(@Param('id') id: string) { 60 | await this.unreadNotification.execute({ 61 | notificationId: id, 62 | }); 63 | } 64 | 65 | @Post() 66 | async create(@Body() body: CreateNotificationBody) { 67 | const { recipientId, content, category } = body; 68 | 69 | const { notification } = await this.sendNotification.execute({ 70 | recipientId, 71 | content, 72 | category, 73 | }); 74 | 75 | return { 76 | notification: NotificationViewModel.toHTTP(notification), 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Insomnia.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2022-12-09T18:29:51.741Z","__export_source":"insomnia.desktop.app:v2022.6.0","resources":[{"_id":"req_eba2133686c147ec912eb0c8758fd9b9","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610558741,"created":1670435239034,"url":"{{host}}/notifications","name":"Create Notification","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"recipientId\": \"f18d98b4-e276-4e96-aa65-df31d70309a8\",\n\t\"content\": \"aasdasds\",\n\t\"category\": \"asdfzcxv\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1670435239034,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_8142e10c198449539f8199e05e4c6885","parentId":null,"modified":1670435223151,"created":1670435223151,"name":"Ignite Lab 04 Node.js","description":"","scope":"collection","_type":"workspace"},{"_id":"req_fa1ff7909a5e4db29eb1dc2739710aed","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610546071,"created":1670610495346,"url":"{{host}}/notifications/count/from/{% response 'body', 'req_eba2133686c147ec912eb0c8758fd9b9', 'b64::JC5ub3RpZmljYXRpb24ucmVjaXBpZW50SWQ=::46b', 'never', 60 %}","name":"Count from recipient","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1670390653325,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7e5a3e37dc4a4fd18aa1baec06eea00d","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610573039,"created":1670610569699,"url":"{{host}}/notifications/from/{% response 'body', 'req_eba2133686c147ec912eb0c8758fd9b9', 'b64::JC5ub3RpZmljYXRpb24ucmVjaXBpZW50SWQ=::46b', 'never', 60 %}","name":"Get from recipient","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1670368360470.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_5b0e2cdaf255425fbae163cebbcdcf5f","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610401616,"created":1670610245876,"url":"{{host}}/notifications/{% response 'body', 'req_eba2133686c147ec912eb0c8758fd9b9', 'b64::JC5ub3RpZmljYXRpb24uaWQ=::46b', 'no-history', 60 %}/cancel","name":"Cancel Notification","description":"","method":"PATCH","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1670346067616,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b84c168f9b534b818732ac27a6092c2b","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610413530,"created":1670610412082,"url":"{{host}}/notifications/{% response 'body', 'req_eba2133686c147ec912eb0c8758fd9b9', 'b64::JC5ub3RpZmljYXRpb24uaWQ=::46b', 'no-history', 60 %}/read","name":"Read Notification","description":"","method":"PATCH","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1670301481907,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_ecd4e06668524a478d39bf49e1999ccf","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610420821,"created":1670610418582,"url":"{{host}}/notifications/{% response 'body', 'req_eba2133686c147ec912eb0c8758fd9b9', 'b64::JC5ub3RpZmljYXRpb24uaWQ=::46b', 'no-history', 60 %}/unread","name":"Unread Notification","description":"","method":"PATCH","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1670279189052.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_ec54cc251eafcbab20f536b5de96f2b8a3cc0458","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670610258089,"created":1670435223162,"name":"Base Environment","data":{},"dataPropertyOrder":{},"color":null,"isPrivate":false,"metaSortKey":1670435223163,"_type":"environment"},{"_id":"jar_ec54cc251eafcbab20f536b5de96f2b8a3cc0458","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670435223164,"created":1670435223164,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_ece5c24178f34eaaa2db84ff77d5c1ef","parentId":"wrk_8142e10c198449539f8199e05e4c6885","modified":1670435223156,"created":1670435223156,"fileName":"Ignite Lab 04 Node.js","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"env_be2b478f34fa492eaf56863741beb500","parentId":"env_ec54cc251eafcbab20f536b5de96f2b8a3cc0458","modified":1670610287275,"created":1670610260258,"name":"Dev","data":{"host":"http://localhost:3000"},"dataPropertyOrder":{"&":["host"]},"color":null,"isPrivate":false,"metaSortKey":1670610260258,"_type":"environment"}]} --------------------------------------------------------------------------------