├── .eslintrc.js ├── .gitignore ├── README.md ├── libs └── common │ ├── src │ ├── database │ │ ├── abstract.repository.ts │ │ ├── abstract.schema.ts │ │ ├── database.module.ts │ │ └── index.ts │ └── index.ts │ └── tsconfig.lib.json ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src ├── app.module.ts ├── main.ts └── users │ ├── __mocks__ │ └── user.service.mock.ts │ ├── db │ ├── user.repository.ts │ └── user.schema.ts │ ├── dto │ ├── create.user.dto.ts │ ├── index.ts │ └── update.user.dto.ts │ ├── test │ ├── stubs │ │ └── user.stub.ts │ ├── support │ │ └── user.model.ts │ ├── user.controller.spec.ts │ └── user.repository.spec.ts │ ├── users.controller.ts │ ├── users.module.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.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 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /.env 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /libs/common/src/database/abstract.repository.ts: -------------------------------------------------------------------------------- 1 | import { DeleteOptions, UpdateFilter } from "mongodb"; 2 | import mongoose, { Aggregate, AggregateOptions, Document, FilterQuery, Model, PipelineStage, PopulateOptions, ProjectionType, QueryOptions, Types, UpdateQuery } from "mongoose"; 3 | import { Logger, NotFoundException } from "@nestjs/common"; 4 | import { AbstractDocument } from "./abstract.schema"; 5 | 6 | export abstract class AbstractRepository { 7 | constructor( 8 | protected readonly entityModel: Model, 9 | ) { }; 10 | 11 | async create( 12 | document: Omit, 13 | ): Promise { 14 | const entity = new this.entityModel({ 15 | _id: new Types.ObjectId(), 16 | ...document 17 | }); 18 | return ((await entity.save()).toJSON()) as unknown as TDocument 19 | } 20 | 21 | async findOne( 22 | filterQuery: FilterQuery, 23 | projection?: ProjectionType, 24 | options?: QueryOptions 25 | ) { 26 | const document = this.entityModel.findOne( 27 | filterQuery, 28 | projection, 29 | options 30 | ).lean(true).exec(); 31 | if (!document) { 32 | throw new NotFoundException("no documents found") 33 | } 34 | return document 35 | } 36 | 37 | async findByIdAndUpdate( 38 | id: mongoose.ObjectId | string, 39 | update?: UpdateQuery, 40 | options?: QueryOptions 41 | ) { 42 | const document = this.entityModel.findByIdAndUpdate( 43 | id, 44 | update, 45 | options 46 | ).lean(true).exec(); 47 | if (!document) { 48 | throw new NotFoundException("no documents found") 49 | } 50 | return document; 51 | } 52 | 53 | async find( 54 | entityFilterQuery: FilterQuery, 55 | projection?: ProjectionType, 56 | options?: QueryOptions 57 | ) { 58 | const documents = this.entityModel.find( 59 | entityFilterQuery, 60 | projection, 61 | options 62 | ).lean(true).exec(); 63 | if (!documents) { 64 | throw new NotFoundException("no documents found") 65 | } 66 | return documents 67 | } 68 | 69 | async findById( 70 | id: mongoose.ObjectId | string, 71 | projection?: Record, 72 | options?: QueryOptions 73 | ) { 74 | const document = await this.entityModel.findById( 75 | id, 76 | { _id: 0, __v: 0, ...projection }, 77 | options 78 | ).exec(); 79 | if (!document) { 80 | throw new NotFoundException("no documents found") 81 | } 82 | return document; 83 | } 84 | 85 | async findOneAndUpdate( 86 | filterQuery: FilterQuery, 87 | updateQueryData: FilterQuery 88 | ) { 89 | const document = this.entityModel.findOneAndUpdate( 90 | filterQuery, 91 | updateQueryData 92 | ).exec(); 93 | if (!document) { 94 | throw new NotFoundException("no documents found") 95 | } 96 | return document; 97 | } 98 | 99 | async updateMany( 100 | filterQuery: FilterQuery, 101 | updateQueryData: FilterQuery 102 | ) { 103 | const document = this.entityModel.updateMany( 104 | filterQuery, 105 | updateQueryData 106 | ).exec(); 107 | if (!document) { 108 | throw new NotFoundException("no documents found") 109 | } 110 | return document; 111 | } 112 | 113 | async deleteMany( 114 | filterQuery: FilterQuery, 115 | options?: DeleteOptions 116 | ): Promise { 117 | const deleteResult = await this.entityModel.deleteMany(filterQuery); 118 | return deleteResult.deletedCount >= 1 119 | } 120 | 121 | async deleteOne( 122 | filterQuery: FilterQuery 123 | ): Promise { 124 | const deleteResult = await this.entityModel.deleteOne(filterQuery); 125 | return deleteResult.deletedCount >= 1; 126 | } 127 | 128 | async populate( 129 | docs: Array, 130 | options: PopulateOptions | Array 131 | ): Promise> { 132 | return this.entityModel.populate(docs, options); 133 | } 134 | 135 | aggregate( 136 | pipeline: PipelineStage[], 137 | options?: AggregateOptions, 138 | ): Aggregate> { 139 | return this.entityModel.aggregate(pipeline, options) 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /libs/common/src/database/abstract.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema } from "@nestjs/mongoose"; 2 | import { Types } from "mongoose"; 3 | 4 | @Schema() 5 | export abstract class AbstractDocument { 6 | @Prop({ type: Types.ObjectId }) 7 | _id: string 8 | } -------------------------------------------------------------------------------- /libs/common/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ModelDefinition, MongooseModule } from "@nestjs/mongoose"; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Module({ 6 | imports: [ 7 | MongooseModule.forRootAsync({ 8 | useFactory: (configService: ConfigService) => ({ 9 | uri: configService.get("MONGODB_URI") 10 | }), 11 | inject: [ConfigService] 12 | }) 13 | ] 14 | }) 15 | export class DatabaseModule { 16 | static forFeature(models: ModelDefinition[]) { 17 | return MongooseModule.forFeature(models); 18 | } 19 | } -------------------------------------------------------------------------------- /libs/common/src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./abstract.schema" 2 | export * from "./abstract.repository" 3 | export * from "./database.module" -------------------------------------------------------------------------------- /libs/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./database" -------------------------------------------------------------------------------- /libs/common/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/common" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "webpack": true 8 | }, 9 | "projects": { 10 | "common": { 11 | "type": "library", 12 | "root": "libs/common", 13 | "entryFile": "index", 14 | "sourceRoot": "libs/common/src", 15 | "compilerOptions": { 16 | "tsConfigPath": "libs/common/tsconfig.lib.json" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-test", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.0.0", 24 | "@nestjs/config": "^3.2.3", 25 | "@nestjs/core": "^10.0.0", 26 | "@nestjs/mongoose": "^10.0.10", 27 | "@nestjs/platform-express": "^10.0.0", 28 | "dotenv": "^16.4.5", 29 | "mongoose": "^8.6.3", 30 | "reflect-metadata": "^0.1.13", 31 | "rxjs": "^7.8.1" 32 | }, 33 | "devDependencies": { 34 | "@nestjs/cli": "^10.0.0", 35 | "@nestjs/schematics": "^10.0.0", 36 | "@nestjs/testing": "^10.0.0", 37 | "@types/express": "^4.17.17", 38 | "@types/jest": "^29.5.2", 39 | "@types/node": "^20.3.1", 40 | "@types/supertest": "^2.0.12", 41 | "@typescript-eslint/eslint-plugin": "^6.0.0", 42 | "@typescript-eslint/parser": "^6.0.0", 43 | "eslint": "^8.42.0", 44 | "eslint-config-prettier": "^9.0.0", 45 | "eslint-plugin-prettier": "^5.0.0", 46 | "jest": "^29.5.0", 47 | "prettier": "^3.0.0", 48 | "source-map-support": "^0.5.21", 49 | "supertest": "^6.3.3", 50 | "ts-jest": "^29.1.0", 51 | "ts-loader": "^9.4.3", 52 | "ts-node": "^10.9.1", 53 | "tsconfig-paths": "^4.2.0", 54 | "typescript": "^5.1.3" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": ".", 63 | "testRegex": ".*\\.spec\\.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "collectCoverageFrom": [ 68 | "**/*.(t|j)s" 69 | ], 70 | "coverageDirectory": "./coverage", 71 | "testEnvironment": "node", 72 | "roots": [ 73 | "/src/", 74 | "/libs/" 75 | ], 76 | "moduleNameMapper": { 77 | "^@app/common(|/.*)$": "/libs/common/src/$1" 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseModule } from '@app/common'; 2 | import { Module } from '@nestjs/common'; 3 | import { UsersModule } from './users/users.module'; 4 | 5 | @Module({ 6 | imports: [ 7 | DatabaseModule, 8 | UsersModule 9 | ], 10 | controllers: [], 11 | providers: [], 12 | }) 13 | export class AppModule { } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(process.env.PORT || Number(3000)); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /src/users/__mocks__/user.service.mock.ts: -------------------------------------------------------------------------------- 1 | import { userStub } from "../test/stubs/user.stub"; 2 | 3 | export const UsersService = jest.fn().mockReturnValue({ 4 | getUserById: jest.fn().mockResolvedValue(userStub()), 5 | getUsers: jest.fn().mockResolvedValue([userStub()]), 6 | createUser: jest.fn().mockResolvedValue(userStub()), 7 | updateUser: jest.fn().mockResolvedValue(userStub()), 8 | }) -------------------------------------------------------------------------------- /src/users/db/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { InjectModel } from "@nestjs/mongoose"; 3 | import { FilterQuery, Model } from "mongoose"; 4 | import { AbstractRepository } from "@app/common"; 5 | import { User } from "./user.schema"; 6 | 7 | @Injectable() 8 | export class UsersRepository extends AbstractRepository { 9 | constructor( 10 | @InjectModel(User.name) 11 | private userSchema: Model 12 | ) { 13 | super(userSchema); 14 | } 15 | } -------------------------------------------------------------------------------- /src/users/db/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { AbstractDocument } from "@app/common"; 2 | import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; 3 | import { Document } from 'mongoose'; 4 | 5 | export type UserDocument = User & Document; 6 | 7 | @Schema() 8 | export class User extends AbstractDocument { 9 | @Prop() 10 | userId: string; 11 | 12 | @Prop() 13 | email: string; 14 | 15 | @Prop() 16 | age: number; 17 | 18 | @Prop([String]) 19 | favoriteFoods: string[] 20 | } 21 | 22 | export const UserSchema = SchemaFactory.createForClass(User); -------------------------------------------------------------------------------- /src/users/dto/create.user.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateUserDto { 2 | email: string; 3 | age: number; 4 | } -------------------------------------------------------------------------------- /src/users/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create.user.dto" 2 | export * from "./update.user.dto" -------------------------------------------------------------------------------- /src/users/dto/update.user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UpdateUserDto { 2 | favoriteFoods: string[] 3 | age: number; 4 | } -------------------------------------------------------------------------------- /src/users/test/stubs/user.stub.ts: -------------------------------------------------------------------------------- 1 | import { User } from "src/users/db/user.schema" 2 | 3 | export const userStub = (): User => { 4 | return { 5 | _id: "object id", 6 | userId: 'sadra3523423', 7 | email: 'sadra@test.com', 8 | age: 22, 9 | favoriteFoods: ['cake', 'pizza'] 10 | } 11 | } -------------------------------------------------------------------------------- /src/users/test/support/user.model.ts: -------------------------------------------------------------------------------- 1 | import { MockModel } from "../../../database/test/support/mock.model"; 2 | import { User } from "../../schemas/user.schema"; 3 | import { userStub } from "../stubs/user.stub"; 4 | 5 | export class UserModel extends MockModel { 6 | protected entityStub = userStub() 7 | } 8 | -------------------------------------------------------------------------------- /src/users/test/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from "@nestjs/testing" 2 | import { CreateUserDto } from "../dto/create-user.dto"; 3 | import { UpdateUserDto } from "../dto/update-user.dto"; 4 | import { User } from "../schemas/user.schema"; 5 | import { UsersController } from "../users.controller" 6 | import { UsersService } from "../users.service" 7 | import { userStub } from "./stubs/user.stub"; 8 | 9 | jest.mock('../users.service'); 10 | 11 | describe('UsersController', () => { 12 | let usersController: UsersController; 13 | let usersService: UsersService; 14 | 15 | beforeEach(async () => { 16 | const moduleRef = await Test.createTestingModule({ 17 | imports: [], 18 | controllers: [UsersController], 19 | providers: [UsersService] 20 | }).compile(); 21 | 22 | usersController = moduleRef.get(UsersController); 23 | usersService = moduleRef.get(UsersService); 24 | jest.clearAllMocks(); 25 | }) 26 | 27 | describe('getUser', () => { 28 | describe('when getUser is called', () => { 29 | let user: User; 30 | 31 | beforeEach(async () => { 32 | user = await usersController.getUser(userStub().userId) 33 | }) 34 | 35 | test('then it should call usersService', () => { 36 | expect(usersService.getUserById).toBeCalledWith(userStub().userId); 37 | }) 38 | 39 | test('then is should return a user', () => { 40 | expect(user).toEqual(userStub()); 41 | }) 42 | }) 43 | }) 44 | 45 | describe('getUsers', () => { 46 | describe('when getUsers is called', () => { 47 | let users: User[]; 48 | 49 | beforeEach(async () => { 50 | users = await usersController.getUsers(); 51 | }) 52 | 53 | test('then it should call usersService', () => { 54 | expect(usersService.getUsers).toHaveBeenCalled(); 55 | }) 56 | 57 | test('then it should return users', () => { 58 | expect(users).toEqual([userStub()]) 59 | }) 60 | }) 61 | }) 62 | 63 | describe('createUser', () => { 64 | describe('when createUser is called', () => { 65 | let user: User; 66 | let createUserDto: CreateUserDto 67 | 68 | beforeEach(async () => { 69 | createUserDto = { 70 | email: userStub().email, 71 | age: userStub().age 72 | } 73 | user = await usersController.createUser(createUserDto); 74 | }) 75 | 76 | test('then it should call usersService', () => { 77 | expect(usersService.createUser).toHaveBeenCalledWith(createUserDto.email, createUserDto.age); 78 | }) 79 | 80 | test('then it should return a user', () => { 81 | expect(user).toEqual(userStub()) 82 | }) 83 | }) 84 | }) 85 | 86 | describe('updateUser', () => { 87 | describe('when updateUser is called', () => { 88 | let user: User; 89 | let updateUserDto: UpdateUserDto; 90 | 91 | beforeEach(async () => { 92 | updateUserDto = { 93 | age: 98, 94 | favoriteFoods: ['pizza'] 95 | } 96 | user = await usersController.updateUser(userStub().userId, updateUserDto); 97 | }) 98 | 99 | test('then it should call usersService', () => { 100 | expect(usersService.updateUser).toHaveBeenCalledWith(userStub().userId, updateUserDto); 101 | }) 102 | 103 | test('then it should return a user', () => { 104 | expect(user).toEqual(userStub()) 105 | }) 106 | }) 107 | }) 108 | }) -------------------------------------------------------------------------------- /src/users/test/user.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { getModelToken } from "@nestjs/mongoose" 2 | import { Test } from "@nestjs/testing" 3 | import { FilterQuery } from "mongoose" 4 | import { User } from "../schemas/user.schema" 5 | import { UsersRepository } from "../users.repository" 6 | import { userStub } from "./stubs/user.stub" 7 | import { UserModel } from "./support/user.model" 8 | 9 | describe('UsersRepository', () => { 10 | let usersRepository: UsersRepository; 11 | 12 | describe('find operations', () => { 13 | let userModel: UserModel; 14 | let userFilterQuery: FilterQuery; 15 | 16 | beforeEach(async () => { 17 | const moduleRef = await Test.createTestingModule({ 18 | providers: [ 19 | UsersRepository, 20 | { 21 | provide: getModelToken(User.name), 22 | useClass: UserModel 23 | } 24 | ] 25 | }).compile(); 26 | 27 | usersRepository = moduleRef.get(UsersRepository); 28 | userModel = moduleRef.get(getModelToken(User.name)); 29 | 30 | userFilterQuery = { 31 | userId: userStub().userId 32 | } 33 | 34 | jest.clearAllMocks(); 35 | }) 36 | 37 | 38 | describe('findOne', () => { 39 | describe('when findOne is called', () => { 40 | let user: User; 41 | 42 | beforeEach(async () => { 43 | jest.spyOn(userModel, 'findOne'); 44 | user = await usersRepository.findOne( 45 | userFilterQuery 46 | ); 47 | }) 48 | 49 | test('then it should call the userModel', () => { 50 | expect( 51 | userModel.findOne 52 | ).toHaveBeenCalledWith( 53 | userFilterQuery, 54 | { 55 | _id: 0, __v: 0 56 | } 57 | ); 58 | }) 59 | 60 | test('then it should return a user', () => { 61 | expect(user).toEqual(userStub()); 62 | }) 63 | }) 64 | }) 65 | 66 | describe('find', () => { 67 | describe('when find is called', () => { 68 | let users: User[]; 69 | 70 | beforeEach(async () => { 71 | jest.spyOn(userModel, 'find'); 72 | users = await usersRepository.find( 73 | userFilterQuery 74 | ); 75 | }) 76 | 77 | test('then it should call the userModel', () => { 78 | expect( 79 | userModel.find 80 | ).toHaveBeenCalledWith( 81 | userFilterQuery 82 | ); 83 | }) 84 | 85 | test('then it should return a user', () => { 86 | expect(users).toEqual([userStub()]); 87 | }) 88 | }) 89 | }) 90 | 91 | describe('findOneAndUpdate', () => { 92 | describe('when findOneAndUpdate is called', () => { 93 | let user: User; 94 | 95 | beforeEach(async () => { 96 | jest.spyOn( 97 | userModel, 98 | 'findOneAndUpdate' 99 | ); 100 | user = await usersRepository.findOneAndUpdate( 101 | userFilterQuery, 102 | userStub() 103 | ); 104 | }) 105 | 106 | test('then it should call the userModel', () => { 107 | expect( 108 | userModel.findOneAndUpdate 109 | ).toHaveBeenCalledWith( 110 | userFilterQuery, 111 | userStub(), 112 | { 113 | new: true 114 | } 115 | ); 116 | }) 117 | 118 | test('then it should return a user', () => { 119 | expect(user).toEqual(userStub()); 120 | }) 121 | }) 122 | }) 123 | }) 124 | describe('create operations', () => { 125 | beforeEach(async () => { 126 | const moduleRef = await Test.createTestingModule({ 127 | providers: [ 128 | UsersRepository, 129 | { 130 | provide: getModelToken(User.name), 131 | useValue: UserModel, 132 | }, 133 | ], 134 | }).compile(); 135 | 136 | usersRepository = moduleRef.get(UsersRepository); 137 | }); 138 | 139 | describe('create', () => { 140 | describe('when create is called', () => { 141 | let user: User; 142 | let saveSpy: jest.SpyInstance; 143 | let constructorSpy: jest.SpyInstance; 144 | 145 | beforeEach(async () => { 146 | saveSpy = jest.spyOn( 147 | UserModel.prototype, 148 | 'save' 149 | ); 150 | constructorSpy = jest.spyOn( 151 | UserModel.prototype, 152 | 'constructorSpy' 153 | ); 154 | user = await usersRepository.create( 155 | userStub() 156 | ); 157 | }) 158 | 159 | test('then it should call the userModel', () => { 160 | expect(saveSpy).toHaveBeenCalled(); 161 | expect(constructorSpy).toHaveBeenCalledWith(userStub()) 162 | }) 163 | 164 | test('then it should return a user', () => { 165 | expect(user).toEqual(userStub()); 166 | }) 167 | }) 168 | }) 169 | }) 170 | }) 171 | 172 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { User } from './db/user.schema'; 4 | import { CreateUserDto, UpdateUserDto } from './dto'; 5 | 6 | 7 | @Controller('users') 8 | export class UsersController { 9 | constructor(private readonly usersService: UsersService) { } 10 | 11 | @Get(':userId') 12 | async getUser( 13 | @Param('userId') userId: string 14 | ): Promise { 15 | return this.usersService.getUserById(userId); 16 | } 17 | 18 | @Get() 19 | async getUsers(): Promise { 20 | return this.usersService.getUsers(); 21 | } 22 | 23 | @Post() 24 | async createUser( 25 | @Body() createUserDto: CreateUserDto 26 | ): Promise { 27 | return this.usersService.createUser(createUserDto.email, createUserDto.age) 28 | } 29 | 30 | @Patch(':userId') 31 | async updateUser( 32 | @Param('userId') userId: string, 33 | @Body() updateUserDto: UpdateUserDto 34 | ): Promise { 35 | return this.usersService.updateUser(userId, updateUserDto); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | import { MongooseModule } from '@nestjs/mongoose'; 5 | import { User, UserSchema } from './db/user.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ 10 | name: User.name, schema: UserSchema 11 | }])], 12 | providers: [UsersService], 13 | controllers: [UsersController] 14 | }) 15 | export class UsersModule { } 16 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UsersRepository } from './db/user.repository'; 3 | import { User } from './db/user.schema'; 4 | import { ObjectId } from 'mongodb'; 5 | import { UpdateUserDto } from './dto'; 6 | 7 | @Injectable() 8 | export class UsersService { 9 | constructor( 10 | private readonly usersRepository: UsersRepository 11 | ) { } 12 | 13 | async getUserById( 14 | userId: string 15 | ): Promise { 16 | return this.usersRepository.findOne({ userId }) 17 | } 18 | 19 | async getUsers(): Promise { 20 | return this.usersRepository.find({}); 21 | } 22 | 23 | async createUser( 24 | email: string, 25 | age: number 26 | ): Promise { 27 | return this.usersRepository.create({ 28 | userId: new ObjectId().toHexString(), 29 | email, 30 | age, 31 | favoriteFoods: [] 32 | }) 33 | } 34 | 35 | async updateUser( 36 | userId: string, 37 | userUpdates: UpdateUserDto 38 | ): Promise { 39 | return this.usersRepository.findOneAndUpdate({ userId }, userUpdates); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | }, 13 | "moduleNameMapper": { 14 | "@app/common/(.*)": "/../libs/common/src/$1", 15 | "@app/common": "/../libs/common/src" 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } -------------------------------------------------------------------------------- /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": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "paths": { 21 | "@app/common": [ 22 | "libs/common/src" 23 | ], 24 | "@app/common/*": [ 25 | "libs/common/src/*" 26 | ] 27 | } 28 | } 29 | } --------------------------------------------------------------------------------