├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── ormconfig.ts ├── package-lock.json ├── package.json ├── requests.http ├── src ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── car │ ├── car.controller.ts │ ├── car.module.ts │ ├── car.service.ts │ ├── dto │ │ ├── create-car.dto.ts │ │ └── update-car.dto.ts │ └── entity │ │ └── car.entity.ts ├── main.ts └── person │ ├── dto │ ├── create-person.dto.ts │ └── update-person.dto.ts │ ├── entity │ └── person.entity.ts │ ├── person.controller.ts │ ├── person.module.ts │ ├── person.service.spec.ts │ └── person.service.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | ### VisualStudio template 53 | ## Ignore Visual Studio temporary files, build results, and 54 | ## files generated by popular Visual Studio add-ons. 55 | ## 56 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 57 | 58 | # User-specific files 59 | *.suo 60 | *.user 61 | *.userosscache 62 | *.sln.docstates 63 | 64 | # User-specific files (MonoDevelop/Xamarin Studio) 65 | *.userprefs 66 | 67 | # Build results 68 | [Dd]ebug/ 69 | [Dd]ebugPublic/ 70 | [Rr]elease/ 71 | [Rr]eleases/ 72 | x64/ 73 | x86/ 74 | bld/ 75 | [Bb]in/ 76 | [Oo]bj/ 77 | [Ll]og/ 78 | 79 | # Visual Studio 2015 cache/options directory 80 | .vs/ 81 | # Uncomment if you have tasks that create the project's static files in wwwroot 82 | #wwwroot/ 83 | 84 | # MSTest test Results 85 | [Tt]est[Rr]esult*/ 86 | [Bb]uild[Ll]og.* 87 | 88 | # NUNIT 89 | *.VisualState.xml 90 | TestResult.xml 91 | 92 | # Build Results of an ATL Project 93 | [Dd]ebugPS/ 94 | [Rr]eleasePS/ 95 | dlldata.c 96 | 97 | # Benchmark Results 98 | BenchmarkDotNet.Artifacts/ 99 | 100 | # .NET Core 101 | project.lock.json 102 | project.fragment.lock.json 103 | artifacts/ 104 | **/Properties/launchSettings.json 105 | 106 | *_i.c 107 | *_p.c 108 | *_i.h 109 | *.ilk 110 | *.meta 111 | *.obj 112 | *.pch 113 | *.pdb 114 | *.pgc 115 | *.pgd 116 | *.rsp 117 | *.sbr 118 | *.tlb 119 | *.tli 120 | *.tlh 121 | *.tmp 122 | *.tmp_proj 123 | *.log 124 | *.vspscc 125 | *.vssscc 126 | .builds 127 | *.pidb 128 | *.svclog 129 | *.scc 130 | 131 | # Chutzpah Test files 132 | _Chutzpah* 133 | 134 | # Visual C++ cache files 135 | ipch/ 136 | *.aps 137 | *.ncb 138 | *.opendb 139 | *.opensdf 140 | *.sdf 141 | *.cachefile 142 | *.VC.db 143 | *.VC.VC.opendb 144 | 145 | # Visual Studio profiler 146 | *.psess 147 | *.vsp 148 | *.vspx 149 | *.sap 150 | 151 | # Visual Studio Trace Files 152 | *.e2e 153 | 154 | # TFS 2012 Local Workspace 155 | $tf/ 156 | 157 | # Guidance Automation Toolkit 158 | *.gpState 159 | 160 | # ReSharper is a .NET coding add-in 161 | _ReSharper*/ 162 | *.[Rr]e[Ss]harper 163 | *.DotSettings.user 164 | 165 | # JustCode is a .NET coding add-in 166 | .JustCode 167 | 168 | # TeamCity is a build add-in 169 | _TeamCity* 170 | 171 | # DotCover is a Code Coverage Tool 172 | *.dotCover 173 | 174 | # AxoCover is a Code Coverage Tool 175 | .axoCover/* 176 | !.axoCover/settings.json 177 | 178 | # Visual Studio code coverage results 179 | *.coverage 180 | *.coveragexml 181 | 182 | # NCrunch 183 | _NCrunch_* 184 | .*crunch*.local.xml 185 | nCrunchTemp_* 186 | 187 | # MightyMoose 188 | *.mm.* 189 | AutoTest.Net/ 190 | 191 | # Web workbench (sass) 192 | .sass-cache/ 193 | 194 | # Installshield output folder 195 | [Ee]xpress/ 196 | 197 | # DocProject is a documentation generator add-in 198 | DocProject/buildhelp/ 199 | DocProject/Help/*.HxT 200 | DocProject/Help/*.HxC 201 | DocProject/Help/*.hhc 202 | DocProject/Help/*.hhk 203 | DocProject/Help/*.hhp 204 | DocProject/Help/Html2 205 | DocProject/Help/html 206 | 207 | # Click-Once directory 208 | publish/ 209 | 210 | # Publish Web Output 211 | *.[Pp]ublish.xml 212 | *.azurePubxml 213 | # Note: Comment the next line if you want to checkin your web deploy settings, 214 | # but database connection strings (with potential passwords) will be unencrypted 215 | *.pubxml 216 | *.publishproj 217 | 218 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 219 | # checkin your Azure Web App publish settings, but sensitive information contained 220 | # in these scripts will be unencrypted 221 | PublishScripts/ 222 | 223 | # NuGet Packages 224 | *.nupkg 225 | # The packages folder can be ignored because of Package Restore 226 | **/[Pp]ackages/* 227 | # except build/, which is used as an MSBuild target. 228 | !**/[Pp]ackages/build/ 229 | # Uncomment if necessary however generally it will be regenerated when needed 230 | #!**/[Pp]ackages/repositories.config 231 | # NuGet v3's project.json files produces more ignorable files 232 | *.nuget.props 233 | *.nuget.targets 234 | 235 | # Microsoft Azure Build Output 236 | csx/ 237 | *.build.csdef 238 | 239 | # Microsoft Azure Emulator 240 | ecf/ 241 | rcf/ 242 | 243 | # Windows Store app package directories and files 244 | AppPackages/ 245 | BundleArtifacts/ 246 | Package.StoreAssociation.xml 247 | _pkginfo.txt 248 | *.appx 249 | 250 | # Visual Studio cache files 251 | # files ending in .cache can be ignored 252 | *.[Cc]ache 253 | # but keep track of directories ending in .cache 254 | !*.[Cc]ache/ 255 | 256 | # Others 257 | ClientBin/ 258 | ~$* 259 | *~ 260 | *.dbmdl 261 | *.dbproj.schemaview 262 | *.jfm 263 | *.pfx 264 | *.publishsettings 265 | orleans.codegen.cs 266 | 267 | # Since there are multiple workflows, uncomment next line to ignore bower_components 268 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 269 | #bower_components/ 270 | 271 | # RIA/Silverlight projects 272 | Generated_Code/ 273 | 274 | # Backup & report files from converting an old project file 275 | # to a newer Visual Studio version. Backup files are not needed, 276 | # because we have git ;-) 277 | _UpgradeReport_Files/ 278 | Backup*/ 279 | UpgradeLog*.XML 280 | UpgradeLog*.htm 281 | 282 | # SQL Server files 283 | *.mdf 284 | *.ldf 285 | *.ndf 286 | 287 | # Business Intelligence projects 288 | *.rdl.data 289 | *.bim.layout 290 | *.bim_*.settings 291 | 292 | # Microsoft Fakes 293 | FakesAssemblies/ 294 | 295 | # GhostDoc plugin setting file 296 | *.GhostDoc.xml 297 | 298 | # Node.js Tools for Visual Studio 299 | .ntvs_analysis.dat 300 | node_modules/ 301 | 302 | # Typescript v1 declaration files 303 | typings/ 304 | 305 | # Visual Studio 6 build log 306 | *.plg 307 | 308 | # Visual Studio 6 workspace options file 309 | *.opt 310 | 311 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 312 | *.vbw 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # JetBrains Rider 330 | .idea/ 331 | *.sln.iml 332 | 333 | # IDE - VSCode 334 | .vscode/* 335 | !.vscode/settings.json 336 | !.vscode/tasks.json 337 | !.vscode/launch.json 338 | !.vscode/extensions.json 339 | 340 | # CodeRush 341 | .cr/ 342 | 343 | # Python Tools for Visual Studio (PTVS) 344 | __pycache__/ 345 | *.pyc 346 | 347 | # Cake - Uncomment if you are using it 348 | # tools/** 349 | # !tools/packages.config 350 | 351 | # Tabs Studio 352 | *.tss 353 | 354 | # Telerik's JustMock configuration file 355 | *.jmconfig 356 | 357 | # BizTalk build output 358 | *.btp.cs 359 | *.btm.cs 360 | *.odx.cs 361 | *.xsd.cs 362 | 363 | # OpenCover UI analysis results 364 | OpenCover/ 365 | coverage/ 366 | 367 | ### macOS template 368 | # General 369 | .DS_Store 370 | .AppleDouble 371 | .LSOverride 372 | 373 | # Icon must end with two \r 374 | Icon 375 | 376 | # Thumbnails 377 | ._* 378 | 379 | # Files that might appear in the root of a volume 380 | .DocumentRevisions-V100 381 | .fseventsd 382 | .Spotlight-V100 383 | .TemporaryItems 384 | .Trashes 385 | .VolumeIcon.icns 386 | .com.apple.timemachine.donotpresent 387 | 388 | # Directories potentially created on remote AFP share 389 | .AppleDB 390 | .AppleDesktop 391 | Network Trash Folder 392 | Temporary Items 393 | .apdisk 394 | 395 | ======= 396 | # Local 397 | .env 398 | dist 399 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with TypeORM for NestJS 2 | [![Presentation](http://img.youtube.com/vi/woMPn3Nm3aA/0.jpg)](http://www.youtube.com/watch?v=woMPn3Nm3aA "Getting Started with TypeORM for NestJS by Sean Ramocki") 3 | 4 | 5 | ## Description 6 | 7 | NestJS Example repository for Postgres TypeORM integration with migrations 8 | 9 | ## Installation 10 | 11 | ```bash 12 | $ npm install 13 | ``` 14 | 15 | ## Running the app 16 | 17 | ```bash 18 | # development 19 | $ npm run start 20 | 21 | # watch mode 22 | $ npm run start:dev 23 | 24 | # production mode 25 | $ npm run start:prod 26 | ``` 27 | 28 | ## Test 29 | 30 | ```bash 31 | # unit tests 32 | $ npm run test 33 | ``` 34 | 35 | ## Migrations 36 | 37 | ```bash 38 | # Create a migration 39 | $ npm run migration:create --name=foo 40 | 41 | # Generate a migration from schema changes 42 | $ npm run migration:generate --name=bar 43 | 44 | # Run migrations and checks for schema changes 45 | $ npm run migration:run 46 | 47 | # Revert migrations 48 | $ npm run migration:revert 49 | 50 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /ormconfig.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | 3 | export const AppDataSource = new DataSource({ 4 | type: 'postgres', 5 | host: 'localhost', 6 | port: 5432, 7 | username: 'postgres', 8 | password: 'admin', 9 | database: 'postgres', 10 | entities: ['dist/**/*.entity.js'], 11 | logging: true, 12 | synchronize: false, 13 | migrationsRun: false, 14 | migrations: ['dist/**/migrations/*.js'], 15 | migrationsTableName: 'history' 16 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-type-orm-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "NestJS TypeORM example repository", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.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 | "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ormconfig.ts", 22 | "migration:create": "cross-var ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:create ./src/migrations/$npm_config_name", 23 | "migration:generate": "cross-var npm run typeorm -- migration:generate ./src/migrations/$npm_config_name", 24 | "migration:run": "npm run build && npm run typeorm -- migration:run", 25 | "migration:revert": "npm run typeorm -- migration:revert" 26 | }, 27 | "dependencies": { 28 | "@nestjs/common": "^8.1.1", 29 | "@nestjs/config": "^2.0.1", 30 | "@nestjs/core": "^8.1.1", 31 | "@nestjs/platform-express": "^8.1.1", 32 | "@nestjs/typeorm": "^8.0.4", 33 | "class-validator": "^0.13.2", 34 | "cross-var": "^1.1.0", 35 | "dotenv": "^16.0.1", 36 | "pg": "^8.7.3", 37 | "reflect-metadata": "^0.1.13", 38 | "rimraf": "^3.0.2", 39 | "rxjs": "^7.4.0", 40 | "typeorm": "^0.3.6" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^8.1.3", 44 | "@nestjs/schematics": "^8.0.4", 45 | "@nestjs/testing": "^8.1.1", 46 | "@types/express": "^4.17.13", 47 | "@types/jest": "^27.0.2", 48 | "@types/node": "^16.11.36", 49 | "@types/supertest": "^2.0.11", 50 | "@typescript-eslint/eslint-plugin": "^4.29.2", 51 | "@typescript-eslint/parser": "^4.29.2", 52 | "eslint": "^7.32.0", 53 | "eslint-config-prettier": "^8.3.0", 54 | "eslint-plugin-prettier": "^3.4.1", 55 | "jest": "^27.3.0", 56 | "prettier": "^2.4.1", 57 | "source-map-support": "^0.5.20", 58 | "supertest": "^6.1.6", 59 | "ts-jest": "^27.0.7", 60 | "ts-loader": "^9.2.6", 61 | "ts-node": "^10.8.0", 62 | "tsconfig-paths": "^3.11.0", 63 | "typescript": "^4.4.4" 64 | }, 65 | "jest": { 66 | "moduleFileExtensions": [ 67 | "js", 68 | "json", 69 | "ts" 70 | ], 71 | "rootDir": "src", 72 | "testRegex": ".*\\.spec\\.ts$", 73 | "transform": { 74 | "^.+\\.(t|j)s$": "ts-jest" 75 | }, 76 | "collectCoverageFrom": [ 77 | "**/*.(t|j)s" 78 | ], 79 | "coverageDirectory": "../coverage", 80 | "testEnvironment": "node" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /requests.http: -------------------------------------------------------------------------------- 1 | ### Get All People 2 | GET http://localhost:3000/person HTTP/1.1 3 | 4 | 5 | ### Get Person by ID 6 | GET http://localhost:3000/person/1 HTTP/1.1 7 | 8 | 9 | ### Create a Person 10 | POST http://localhost:3000/person HTTP/1.1 11 | content-type: application/json 12 | 13 | { 14 | "firstName": "Sean", 15 | "lastName": "Ramocki", 16 | "age": "85" 17 | } 18 | 19 | ### Update a Person 20 | PUT http://localhost:3000/person/1 HTTP/1.1 21 | content-type: application/json 22 | 23 | { 24 | "age": "45" 25 | } 26 | 27 | ### Delete a Person 28 | DELETE http://localhost:3000/person/2 HTTP/1.1 29 | 30 | ### Get All Cars 31 | GET http://localhost:3000/car HTTP/1.1 32 | 33 | ### Create a Car 34 | POST http://localhost:3000/car HTTP/1.1 35 | content-type: application/json 36 | 37 | { 38 | "model": "Venza", 39 | "make": "Toyota", 40 | "person": 1 41 | } 42 | 43 | ### Test Query 44 | PATCH http://localhost:3000/person HTTP/1.1 -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule } from '@nestjs/config' 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { AppDataSource } from 'ormconfig' 7 | import { PersonModule } from './person/person.module'; 8 | import { CarModule } from './car/car.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot(), 13 | TypeOrmModule.forRoot(AppDataSource.options), 14 | PersonModule, 15 | CarModule 16 | ], 17 | controllers: [AppController], 18 | providers: [AppService], 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/car/car.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; 2 | import { CreateCarDto } from './dto/create-car.dto'; 3 | import { UpdateCarDto } from './dto/update-car.dto'; 4 | import { CarService } from './car.service'; 5 | import { Car } from './entity/car.entity'; 6 | 7 | @Controller('car') 8 | export class CarController { 9 | constructor(private readonly carService: CarService) {} 10 | 11 | @Get() 12 | async getAllCar(): Promise { 13 | return this.carService.getAllCars(); 14 | } 15 | 16 | @Get(':id') 17 | async getCarById(@Param('id') id: string): Promise { 18 | return this.carService.getCarById(Number(id)); 19 | } 20 | 21 | @Post() 22 | async createCar(@Body() createCarDto: CreateCarDto): Promise { 23 | return this.carService.createCar(createCarDto); 24 | } 25 | 26 | @Put(':id') 27 | async updateCar(@Param('id') id: string, @Body() updateCarDto: UpdateCarDto): Promise { 28 | return this.carService.updateCar(Number(id), updateCarDto); 29 | } 30 | 31 | @Delete(':id') 32 | async deleteCar(@Param('id') id: string) { 33 | return this.carService.deleteCar(Number(id)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/car/car.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CarController } from './car.controller'; 3 | import { CarService } from './car.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Car } from './entity/car.entity'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forFeature([Car]) 10 | ], 11 | controllers: [CarController], 12 | providers: [CarService], 13 | }) 14 | export class CarModule {} 15 | -------------------------------------------------------------------------------- /src/car/car.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm' 3 | import { Repository } from 'typeorm'; 4 | import { CreateCarDto } from './dto/create-car.dto'; 5 | import { UpdateCarDto } from './dto/update-car.dto'; 6 | import { Car } from './entity/car.entity'; 7 | 8 | @Injectable() 9 | export class CarService { 10 | constructor( 11 | @InjectRepository(Car) 12 | private carRepository: Repository 13 | ) {} 14 | 15 | async getAllCars(): Promise { 16 | return this.carRepository.find(); 17 | } 18 | 19 | async getCarById(carId: number): Promise { 20 | const car = await this.carRepository.findOneBy({ id: carId }); 21 | if (car) { 22 | return car; 23 | } 24 | throw new HttpException('Car not found', HttpStatus.NOT_FOUND); 25 | } 26 | 27 | async createCar(createCarDto: CreateCarDto): Promise { 28 | const car = this.carRepository.create(createCarDto); 29 | return this.carRepository.save(car); 30 | } 31 | 32 | async updateCar(carId: number, updateCarDto: UpdateCarDto): Promise { 33 | await this.carRepository.update(carId, updateCarDto) 34 | const person = await this.carRepository.findOneBy({id: carId}) 35 | if (person) { 36 | return person; 37 | } 38 | } 39 | 40 | async deleteCar(carId: number) { 41 | const deletedCar = await this.carRepository.delete({id: carId}) 42 | if (!deletedCar.affected) { 43 | throw new HttpException('Car not found', HttpStatus.NOT_FOUND); 44 | } 45 | return { deleted: true }; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/car/dto/create-car.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsNumber } from 'class-validator' 2 | import { Person } from 'src/person/entity/person.entity' 3 | export class CreateCarDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | model: string 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | make: string 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | color: string 15 | 16 | @IsNotEmpty() 17 | @IsNumber() 18 | personId: Person 19 | } -------------------------------------------------------------------------------- /src/car/dto/update-car.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsInt, Min, Max, IsOptional } from 'class-validator' 2 | export class UpdateCarDto { 3 | @IsOptional() 4 | @IsString() 5 | model: string 6 | 7 | @IsOptional() 8 | @IsString() 9 | make: string 10 | 11 | @IsOptional() 12 | @IsString() 13 | color: string 14 | } -------------------------------------------------------------------------------- /src/car/entity/car.entity.ts: -------------------------------------------------------------------------------- 1 | import { Person } from "src/person/entity/person.entity"; 2 | import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from "typeorm" 3 | 4 | @Entity() 5 | export class Car { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | model: string; 11 | 12 | @Column() 13 | make: string; 14 | 15 | @Column({nullable: true}) 16 | year: number; 17 | 18 | @Column({nullable: true}) 19 | color: string; 20 | 21 | @ManyToOne(() => Person, person => person.cars) 22 | @JoinColumn({name: "personId"}) 23 | person: Person; 24 | } -------------------------------------------------------------------------------- /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(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /src/person/dto/create-person.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsInt, Min, Max } from 'class-validator' 2 | export class CreatePersonDto { 3 | @IsNotEmpty() 4 | @IsString() 5 | firstName: string 6 | 7 | @IsNotEmpty() 8 | @IsString() 9 | lastName: string 10 | 11 | @IsNotEmpty() 12 | @IsInt() 13 | @Min(0) 14 | @Max(120) 15 | age: number 16 | } -------------------------------------------------------------------------------- /src/person/dto/update-person.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, IsInt, Min, Max, IsOptional } from 'class-validator' 2 | export class UpdatePersonDto { 3 | @IsOptional() 4 | @IsString() 5 | firstName: string 6 | 7 | @IsOptional() 8 | @IsString() 9 | lastName: string 10 | 11 | @IsOptional() 12 | @IsInt() 13 | @Min(0) 14 | @Max(120) 15 | age: number 16 | } -------------------------------------------------------------------------------- /src/person/entity/person.entity.ts: -------------------------------------------------------------------------------- 1 | import { Car } from "src/car/entity/car.entity"; 2 | import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from "typeorm" 3 | 4 | @Entity() 5 | export class Person { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | firstName: string; 11 | 12 | @Column() 13 | lastName: string; 14 | 15 | @Column() 16 | age: number; 17 | 18 | @OneToMany(() => Car, car => car.person, { 19 | eager: true}) 20 | cars: Car[]; 21 | 22 | constructor(firstName: string, lastName?: string, age?: number); 23 | constructor(firstName: string, lastName: string, age?: number); 24 | constructor(firstName: string, lastName: string, age: number); 25 | constructor(firstName?: string, lastName?: string, age?: number); 26 | constructor(firstName?: string, lastName?: string, age?: number) { 27 | this.firstName = firstName || ''; 28 | this.lastName = lastName || ''; 29 | this.age = age || NaN; 30 | } 31 | } -------------------------------------------------------------------------------- /src/person/person.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; 2 | import { CreatePersonDto } from './dto/create-person.dto'; 3 | import { UpdatePersonDto } from './dto/update-person.dto'; 4 | import { Person } from './entity/person.entity'; 5 | import { PersonService } from './person.service'; 6 | 7 | @Controller('person') 8 | export class PersonController { 9 | constructor(private readonly personService: PersonService) {} 10 | 11 | @Get() 12 | async getAllPeople(): Promise { 13 | return this.personService.getAllPeople(); 14 | } 15 | 16 | @Get(':id') 17 | async getPersonById(@Param('id') id: string): Promise { 18 | return this.personService.getPersonById(Number(id)); 19 | } 20 | 21 | @Post() 22 | async createPerson(@Body() createPersonDto: CreatePersonDto): Promise { 23 | return this.personService.createPerson(createPersonDto); 24 | } 25 | 26 | @Put(':id') 27 | async updatePerson(@Param('id') id: string, @Body() updatePersonDto: UpdatePersonDto): Promise { 28 | return this.personService.updatePerson(Number(id), updatePersonDto); 29 | } 30 | 31 | @Delete(':id') 32 | async deletePerson(@Param('id') id: string) { 33 | return this.personService.deletePerson(Number(id)); 34 | } 35 | 36 | // Not actually a PATCH, used for testing! 37 | @Patch() 38 | async testQuery(): Promise { 39 | return this.personService.testQuery(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/person/person.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PersonController } from './person.controller'; 3 | import { PersonService } from './person.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Person } from './entity/person.entity'; 6 | 7 | @Module({ 8 | imports: [ 9 | TypeOrmModule.forFeature([Person]) 10 | ], 11 | controllers: [PersonController], 12 | providers: [PersonService], 13 | }) 14 | export class PersonModule {} 15 | -------------------------------------------------------------------------------- /src/person/person.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { DeleteResult, Repository } from 'typeorm'; 5 | import { Person } from './entity/person.entity'; 6 | import { PersonService } from './person.service'; 7 | 8 | const testFirstName1 = 'First 1'; 9 | const testLastName1 = 'Last 1'; 10 | 11 | const personArray = [ 12 | new Person(testFirstName1, testLastName1, 22), 13 | new Person('First 2', 'Last 2', 22), 14 | new Person('First 3', 'Last 3', 22), 15 | ]; 16 | 17 | const onePerson = new Person(testFirstName1, testLastName1, 22); 18 | let deletedResult = new DeleteResult() 19 | deletedResult.affected = 1 20 | 21 | describe('PersonService', () => { 22 | let service: PersonService; 23 | let repo: Repository; 24 | 25 | beforeEach(async () => { 26 | const module: TestingModule = await Test.createTestingModule({ 27 | providers: [ 28 | PersonService, 29 | { 30 | provide: getRepositoryToken(Person), 31 | useValue: { 32 | find: jest.fn().mockResolvedValue(personArray), 33 | findOneBy: jest.fn().mockResolvedValue(onePerson), 34 | create: jest.fn().mockResolvedValue(onePerson), 35 | save: jest.fn().mockResolvedValue(onePerson), 36 | update: jest.fn().mockResolvedValue(onePerson), 37 | delete: jest.fn().mockResolvedValue(deletedResult), 38 | }, 39 | }, 40 | ], 41 | }).compile(); 42 | 43 | service = module.get(PersonService); 44 | repo = module.get>(getRepositoryToken(Person)); 45 | }); 46 | 47 | it('should be defined', () => { 48 | expect(service).toBeDefined(); 49 | }); 50 | describe('getAllPeople', () => { 51 | it('should return an array of persons', async () => { 52 | const persons = await service.getAllPeople(); 53 | expect(persons).toEqual(personArray); 54 | }); 55 | }); 56 | describe('getPersonById', () => { 57 | it('should get a single person', () => { 58 | const repoSpy = jest.spyOn(repo, 'findOneBy'); 59 | expect(service.getPersonById(1)).resolves.toEqual(onePerson); 60 | expect(repoSpy).toBeCalledWith({ id: 1 }); 61 | }); 62 | }); 63 | describe('createPerson', () => { 64 | it('should successfully create a person', async () => { 65 | const createPersonDto = { 66 | firstName: testFirstName1, 67 | lastName: testLastName1, 68 | age: 22, 69 | } 70 | const person = await service.createPerson(createPersonDto); 71 | expect(person).toEqual(onePerson); 72 | expect(repo.create).toBeCalledTimes(1); 73 | expect(repo.create).toBeCalledWith(createPersonDto); 74 | expect(repo.save).toBeCalledTimes(1); 75 | }); 76 | }); 77 | describe('updatePerson', () => { 78 | it('should call the update method', async () => { 79 | const updatePersonDto = { 80 | firstName: testFirstName1, 81 | lastName: testLastName1, 82 | age: 22, 83 | } 84 | const person = await service.updatePerson( 85 | 1, 86 | updatePersonDto 87 | ); 88 | expect(person).toEqual(onePerson); 89 | expect(repo.update).toBeCalledTimes(1); 90 | expect(repo.update).toBeCalledWith( 91 | 1, updatePersonDto); 92 | }); 93 | }); 94 | describe('deletePerson', () => { 95 | it('should return {deleted: true} if deleted', () => { 96 | expect(service.deletePerson(1)).resolves.toEqual({ deleted: true }); 97 | }); 98 | it('should return an http exception if no matches found', () => { 99 | const repoSpy = jest 100 | .spyOn(repo, 'delete') 101 | .mockRejectedValueOnce(new HttpException('Person not found', HttpStatus.NOT_FOUND)); 102 | expect(service.deletePerson(1)).rejects.toThrow('Person not found'); 103 | expect(repoSpy).toBeCalledWith({ id: 1 }); 104 | expect(repoSpy).toBeCalledTimes(1); 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /src/person/person.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm' 3 | import { Repository } from 'typeorm'; 4 | import { CreatePersonDto } from './dto/create-person.dto'; 5 | import { UpdatePersonDto } from './dto/update-person.dto'; 6 | import { Person } from './entity/person.entity'; 7 | 8 | @Injectable() 9 | export class PersonService { 10 | constructor( 11 | @InjectRepository(Person) 12 | private personRepository: Repository 13 | ) {} 14 | 15 | async getAllPeople(): Promise { 16 | return this.personRepository.find(); 17 | } 18 | 19 | async getPersonById(personId: number): Promise { 20 | const person = await this.personRepository.findOneBy({ id: personId }); 21 | if (person) { 22 | return person; 23 | } 24 | throw new HttpException('Person not found', HttpStatus.NOT_FOUND); 25 | } 26 | 27 | async createPerson(createPersonDto: CreatePersonDto): Promise { 28 | const person = this.personRepository.create(createPersonDto); 29 | return this.personRepository.save(person); 30 | } 31 | 32 | async updatePerson(personId: number, updatePersonDto: UpdatePersonDto): Promise { 33 | await this.personRepository.update(personId, updatePersonDto) 34 | const person = await this.personRepository.findOneBy({id: personId}) 35 | if (person) { 36 | return person; 37 | } 38 | } 39 | 40 | async deletePerson(personId: number) { 41 | const deletedPerson = await this.personRepository.delete({id: personId}) 42 | if (!deletedPerson.affected) { 43 | throw new HttpException('Person not found', HttpStatus.NOT_FOUND); 44 | } 45 | return { deleted: true }; 46 | } 47 | 48 | async testQuery(): Promise { 49 | return this.personRepository.createQueryBuilder("person") 50 | .leftJoinAndSelect("person.cars", "car") 51 | //.innerJoinAndSelect("person.cars", "car") 52 | //.where("person.firstName = :name", { name: "Sean" }) 53 | .getOne() 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /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 | } 16 | } 17 | --------------------------------------------------------------------------------