├── .commitlintrc.js ├── .cz-config.js ├── .dockerignore ├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── Dockerfile ├── README.md ├── Taskfile ├── jest.config.js ├── nest-cli.json ├── package-lock.json ├── package.json ├── prisma ├── README.md ├── migrations │ ├── 20200301180958-initial │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ ├── 20200317005941-count-posts │ │ ├── README.md │ │ ├── _after.js │ │ ├── schema.prisma │ │ └── steps.json │ ├── 20200327000541-category │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ └── migrate.lock ├── schema.prisma ├── schema.sql ├── seed.ts └── select-playground.ts ├── src ├── app.config.ts ├── app.module.ts ├── app.resolver.ts ├── app.service.spec.ts ├── app.service.ts ├── app_modules │ ├── nestjs-prisma-select │ │ └── index.ts │ └── nestjs-select-fields │ │ ├── index.ts │ │ ├── select-fields.decorator.spec.ts │ │ └── select-fields.decorator.ts ├── main.ts ├── post │ └── models │ │ └── post.ts ├── prisma │ ├── prisma.module.ts │ ├── prisma.service.spec.ts │ └── prisma.service.ts ├── types │ ├── app.model.ts │ └── index.ts └── user │ ├── create-user.guard.ts │ ├── models │ ├── user-create-input.ts │ └── user.ts │ ├── user.module.ts │ ├── user.repository.ts │ ├── user.resolver.ts │ └── user.service.ts ├── stryker.conf.js ├── test ├── app.spec.ts └── jest-e2e.config.js ├── tsconfig.build.json ├── tsconfig.json └── tslint.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [2, 'always', ['sentence-case']], 5 | 'scope-case': [2, 'always', ['lower-case', 'upper-case']], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: A new feature' }, 4 | { value: 'fix', name: 'fix: A bug fix' }, 5 | { value: 'docs', name: 'docs: Documentation only changes' }, 6 | { 7 | value: 'style', 8 | name: 9 | 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 10 | }, 11 | { 12 | value: 'refactor', 13 | name: 'refactor: A code change that neither fixes a bug nor adds a feature', 14 | }, 15 | { 16 | value: 'perf', 17 | name: 'perf: A code change that improves performance', 18 | }, 19 | { value: 'test', name: 'test: Adding missing tests' }, 20 | { 21 | value: 'chore', 22 | name: 23 | 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', 24 | }, 25 | { value: 'revert', name: 'revert: Revert to a commit' }, 26 | { value: 'WIP', name: 'WIP: Work in progress' }, 27 | ], 28 | 29 | scopes: [{ name: 'other' }], 30 | 31 | allowTicketNumber: true, 32 | isTicketNumberRequired: false, 33 | ticketNumberPrefix: 'ISSUE-', 34 | ticketNumberRegExp: '\\d{1,5}', 35 | 36 | // it needs to match the value for field type. Eg.: 'fix' 37 | /* 38 | scopeOverrides: { 39 | fix: [ 40 | 41 | {name: 'merge'}, 42 | {name: 'style'}, 43 | {name: 'e2eTest'}, 44 | {name: 'unitTest'} 45 | ] 46 | }, 47 | */ 48 | // override the messages, defaults are as follows 49 | messages: { 50 | type: "Select the type of change that you're committing:", 51 | scope: '\nDenote the SCOPE of this change (optional):', 52 | // used if allowCustomScopes is true 53 | customScope: 'Denote the SCOPE of this change:', 54 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 55 | body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', 56 | breaking: 'List any BREAKING CHANGES (optional):\n', 57 | footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', 58 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 59 | }, 60 | 61 | allowCustomScopes: true, 62 | allowBreakingChanges: ['feat', 'fix'], 63 | // skip any questions you want 64 | skipQuestions: ['body', 'footer'], 65 | 66 | // limit subject length 67 | subjectLimit: 100, 68 | // breaklineChar: '|', // It is supported for fields body and footer. 69 | footerPrefix: 'closes:', 70 | // askForBreakingChangeFirst : true, // default is false 71 | upperCaseSubject: true, 72 | }; 73 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !src 3 | !prisma 4 | !package.json 5 | !package-lock.json 6 | !Taskfile 7 | !tsconfig.json 8 | !tsconfig.build.json 9 | !nest-cli.json 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | max_line_length = off 15 | 16 | [*.{json,yml}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # https://github.com/prisma/prisma2/blob/master/docs/core/connectors/postgresql.md#connection-string 2 | DATABASE_URL= 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | 'plugin:unicorn/recommended', 14 | 'plugin:promise/recommended', 15 | 'plugin:sonarjs/recommended', 16 | ], 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | project: 'tsconfig.json', 20 | sourceType: 'module', 21 | }, 22 | plugins: [ 23 | '@typescript-eslint/eslint-plugin', 24 | 'unicorn', 25 | 'import', 26 | 'wix-editor', 27 | '@typescript-eslint/tslint', 28 | 'prettier', 29 | 'simple-import-sort', 30 | 'promise', 31 | 'sonarjs', 32 | 'only-warn', 33 | ], 34 | ignorePatterns: ['src/@generated'], 35 | rules: { 36 | // core 37 | 'no-unused-vars': 0, 38 | 'no-undef': 0, 39 | 'consistent-return': [1, { treatUndefinedAsUnspecified: true }], 40 | quotes: [1, 'single', { allowTemplateLiterals: true, avoidEscape: true }], 41 | semi: [1, 'always'], 42 | 'max-lines': [1, { max: 200 }], 43 | 'max-params': [1, { max: 5 }], 44 | 'no-unneeded-ternary': [1], 45 | // wix-editor 46 | 'wix-editor/no-instanceof-array': 1, 47 | 'wix-editor/no-not-not': 1, 48 | 'wix-editor/no-unneeded-match': 1, 49 | 'wix-editor/prefer-filter': 1, 50 | 'wix-editor/prefer-ternary': 1, 51 | 'wix-editor/return-boolean': 1, 52 | 'wix-editor/simplify-boolean-expression': 1, 53 | // unicorn 54 | 'unicorn/prefer-spread': 0, 55 | 'unicorn/catch-error-name': 0, 56 | 'unicorn/prevent-abbreviations': [ 57 | 1, 58 | { 59 | replacements: { 60 | err: false, 61 | prod: false, 62 | ref: false, 63 | params: false, 64 | args: false, 65 | }, 66 | }, 67 | ], 68 | // import 69 | 'import/newline-after-import': 0, 70 | 'import/no-duplicates': 1, 71 | 'import/max-dependencies': [1, { max: 15 }], 72 | // sort-imports 73 | 'simple-import-sort/sort': 1, 74 | 'prettier/prettier': [1, { endOfLine: 'auto' }], 75 | 'sort-imports': 'off', 76 | 'import/order': 'off', 77 | // tslint 78 | '@typescript-eslint/interface-name-prefix': 'off', 79 | '@typescript-eslint/explicit-function-return-type': 'off', 80 | '@typescript-eslint/no-explicit-any': 'off', 81 | '@typescript-eslint/no-use-before-define': 0, 82 | '@typescript-eslint/no-floating-promises': 1, 83 | '@typescript-eslint/explicit-function-return-type': 0, 84 | '@typescript-eslint/ban-ts-ignore': 0, 85 | '@typescript-eslint/no-unnecessary-condition': [1, { ignoreRhs: true }], 86 | '@typescript-eslint/no-unused-vars': 0, 87 | '@typescript-eslint/tslint/config': [ 88 | 1, 89 | { 90 | lintFile: './tslint.json', 91 | }, 92 | ], 93 | }, 94 | overrides: [ 95 | { 96 | files: ['*.spec.ts', '**/testing/**/*.ts', '*.e2e-spec.ts'], 97 | rules: { 98 | 'unicorn/consistent-function-scoping': 0, 99 | 'unicorn/prevent-abbreviations': 0, 100 | '@typescript-eslint/tslint/config': 0, 101 | 'consistent-return': 0, 102 | 'max-lines': 0, 103 | '@typescript-eslint/no-explicit-any': 0, 104 | '@typescript-eslint/no-floating-promises': 0, 105 | '@typescript-eslint/no-non-null-assertion': 0, 106 | '@typescript-eslint/camelcase': 0, 107 | 'import/max-dependencies': 0, 108 | 'sonarjs/no-duplicate-string': 0, 109 | }, 110 | }, 111 | ], 112 | }; 113 | -------------------------------------------------------------------------------- /.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 | # CodeRush 334 | .cr/ 335 | 336 | # Python Tools for Visual Studio (PTVS) 337 | __pycache__/ 338 | *.pyc 339 | 340 | # Cake - Uncomment if you are using it 341 | # tools/** 342 | # !tools/packages.config 343 | 344 | # Tabs Studio 345 | *.tss 346 | 347 | # Telerik's JustMock configuration file 348 | *.jmconfig 349 | 350 | # BizTalk build output 351 | *.btp.cs 352 | *.btm.cs 353 | *.odx.cs 354 | *.xsd.cs 355 | 356 | # OpenCover UI analysis results 357 | OpenCover/ 358 | coverage/ 359 | 360 | ### macOS template 361 | # General 362 | .DS_Store 363 | .AppleDouble 364 | .LSOverride 365 | 366 | # Icon must end with two \r 367 | Icon 368 | 369 | # Thumbnails 370 | ._* 371 | 372 | # Files that might appear in the root of a volume 373 | .DocumentRevisions-V100 374 | .fseventsd 375 | .Spotlight-V100 376 | .TemporaryItems 377 | .Trashes 378 | .VolumeIcon.icns 379 | .com.apple.timemachine.donotpresent 380 | 381 | # Directories potentially created on remote AFP share 382 | .AppleDB 383 | .AppleDesktop 384 | Network Trash Folder 385 | Temporary Items 386 | .apdisk 387 | 388 | # Local 389 | .env 390 | .env* 391 | dist 392 | ~* 393 | app_bin 394 | .stryker-tmp 395 | stryker-tmp 396 | @generated/ 397 | /prisma/Studio/ 398 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = true 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | trailingComma: 'all', 4 | tabWidth: 4, 5 | semi: true, 6 | singleQuote: true, 7 | overrides: [ 8 | { 9 | files: '*.{json,yml}', 10 | options: { 11 | tabWidth: 2, 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build . -t nestjs-app 2 | FROM node:13 AS base 3 | WORKDIR /app 4 | ENV DATABASE_URL file:data.db 5 | 6 | FROM base AS build 7 | # install dependencies 8 | COPY . . 9 | RUN npm ci 10 | # build sources 11 | RUN npm run build 12 | RUN npm ci --production --ignore-scripts 13 | 14 | # TODO use node-alpine when supported by prisma2 https://github.com/prisma/prisma2/issues/702 15 | FROM base 16 | COPY --from=build /app/node_modules ./node_modules 17 | COPY --from=build /app/dist ./ 18 | CMD ["node", "/app/src/main.js"] 19 | # docker run -it -v "$PWD/data":/data -e DATABASE_URL=file:/data/db.sqlite -p 8080:3000 nestjs-app 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nest-typescript-starter 2 | 3 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 4 | 5 | - [Next version v7 (DOESNT WORK)](./tree/v7) 6 | - [nestjs-graphql-prisma-realworld-example-app](https://github.com/unlight/nestjs-graphql-prisma-realworld-example-app) 7 | - [This version v6](./tree/v6) (OUTDATED) 8 | - [Previous version v5 (TypeScript, ESLint, Mocha, TypeORM, Stryker)](./tree/v5) 9 | 10 | ## Stack 11 | 12 | - NestJS 13 | - TypeScript 14 | - TypeGraphQL 15 | - Prisma 2 16 | - ESLint 17 | - Jest 18 | - Stryker 19 | 20 | ## Installation 21 | 22 | ```bash 23 | $ npm ci 24 | ``` 25 | 26 | ## Tasks 27 | 28 | | Command | Description | 29 | | :------------------- | :---------------------------------- | 30 | | `npm run start` | Running the app in development mode | 31 | | `npm run start:dev` | Running the app in watch mode | 32 | | `npm run start:prod` | Running the app in production mode | 33 | | `npm run test:r` | Unit tests | 34 | | `npm run test:e2e` | E2E tests | 35 | | `npm run test:cov` | Test coverage | 36 | | `npm run test:m` | Run mutation tests | 37 | 38 | ## Project Structure 39 | 40 | - `src` - Source code 41 | - `@generated` - Generated code 42 | - `prisma` - DB toolkit to query, migrate and model your database 43 | 44 | ## Resources 45 | 46 | - https://github.com/nestjs/nest/tree/master/examples 47 | - GDG DevFest 2017 (Presentation) - https://github.com/Caballerog/devfestmalaga2017 48 | - Router Module For Nestjs Framework - https://github.com/shekohex/nest-router 49 | - TypeORM where like expression - https://gitter.im/typeorm/typeorm?at=5a2035af232e79134df08c65 50 | - Add the most common Express middlewares to your Nest app with one line - https://github.com/wbhob/nest-middlewares 51 | - Level up your Node.js application with Nest: Angular sugar on the server - http://ng-atl.org/workshops/level-up-your-node-js-application-with-nest-angular-sugar-on-the-server 52 | - New Stack on the Market to beat them all MANT (MongoDB Angular NestJS TypeScript) - https://github.com/vladotesanovic/mant 53 | - Microservices example - https://github.com/james7272/nestjs-hybrid-example 54 | - nest-graphql-apollo - https://github.com/kamilkisiela/nest-graphql-apollo 55 | - nest-graphql-mongodb - https://github.com/iamclaytonray/nest-graphql-mongodb 56 | - nestjs-graphql - https://github.com/adrien2p/nestjs-graphql 57 | - graphql-js/issues/19 - https://github.com/graphql/graphql-js/issues/19 58 | - graph-type-orm-nest-example - https://github.com/partyka95/graph-type-orm-nest-example 59 | - nest-mailer - https://github.com/partyka95/nest-mailer 60 | - ngrx-nest - https://github.com/derekkite/ngrx-nest 61 | - Simple application demonstrating the basic usage of permissions - https://github.com/rucken/core-nestjs 62 | - Nest + Sequelize + jwt - https://github.com/adrien2p/nestjs-sequelize-jwt 63 | - Awesome Nest - https://github.com/juliandavidmr/awesome-nest 64 | - Learn the nestjs framework in 30 days - https://github.com/m24927605/Nestjs30Days 65 | - A simple web application - https://github.com/chanlito/simple-todos 66 | - A collection of useful modules and opinionated to use with Nest framework - https://github.com/chanlito/nestjs-extensions 67 | - A simple application demonstrating the basic usage of permissions - https://github.com/rucken/core-nestjs 68 | - Opinionated Framework built on top of NestJS and TypeORM - https://github.com/mentos1386/lynx 69 | - This is a Bull module for Nest - https://github.com/fwoelffel/nest-bull 70 | - Hybrid apps have embedded microservices - https://github.com/james7272/nestjs-hybrid-example/ 71 | - I-know-nest repository - https://github.com/cojack/i-know-nest 72 | - A Bull module for Nest framework - https://github.com/fwoelffel/nest-bull 73 | - https://github.com/caiya/graphql-nestjs-typeorm 74 | - Scaffold quickly your next TypeScript API with this opinionated NestJS template crafted for Docker environments - https://github.com/Saluki/nestjs-template 75 | - Authentication and Authorization example for Nest.js TypeScript Framework - https://github.com/neoteric-eu/nestjs-auth 76 | - Another example of chiny app - https://github.com/notadd/notadd 77 | - Another example of nest - https://github.com/bojidaryovchev/nest-angular 78 | - Blog made with nestJS - https://github.com/bashleigh/nestjs-blog 79 | - An easiest web app template on top of nest, TypeORM, Next.js and Material UI - https://github.com/saltyshiomix/ark 80 | - Ever® - On-Demand Commerce Platform (Example Application) - https://github.com/ever-co/ever 81 | - https://github.com/AryanJ-NYC/nestjs-graphql-tutorial 82 | - End to end repo testing all core components in this stack - https://github.com/webnoob/quasar-ts-jest-nestjs-apollojs-prisma2 83 | - graphql-prisma2 with all(okay most of) the required configuration you need - https://github.com/5achinJani/graphql-prisma2 84 | - https://github.com/toondaey/nestjs-boiler 85 | - https://github.com/notiz-dev/nestjs-prisma 86 | - https://github.com/chnirt/nestjs-graphql-best-practice 87 | - Starter template for NestJS includes Graphql with Prisma Client, Passport-JWT authentication, Swagger Api and Docker - https://github.com/fivethree-team/nestjs-prisma-starter 88 | - https://www.npmjs.com/package/typegraphql-prisma 89 | - https://github.com/benawad/nest-mongo-graphql 90 | - https://github.com/MagnusCloudCorp/nestjs-type-graphql 91 | - https://github.com/TimurRK/nestjs-example 92 | - A collection of badass modules and utilities to help you level up your NestJS applications - https://github.com/golevelup/nestjs 93 | - https://github.com/TannerGabriel/nestjs-graphql-boilerplate 94 | - https://github.com/svtslv/nestjs-kubernetes 95 | - https://github.com/lujakob/nestjs-realworld-example-app 96 | - https://github.com/chnirt/nestjs-graphql-best-practice 97 | - https://github.com/unlight/nestjs-graphql-prisma-realworld-example-app 98 | - https://github.com/Sameerkash/Nest-microservcies 99 | - https://github.com/toondaey/nestjs-module-boilerplate 100 | - End to end build a project with NestJS - https://github.com/tienduy-nguyen/nestjs-flow 101 | - https://github.com/tienduy-nguyen/nestjs-graphql-prisma - Boilerplate backend NestJS GraphQL project using Prisma 2 & PostgreSQL 102 | - https://github.com/Sairyss/domain-driven-hexagon 103 | - https://wanago.io/2020/12/07/api-nestjs-introduction-cqrs/ - API with NestJS 104 | - https://github.com/squareboat/nestjs-boilerplate production-ready 🏭 NestJS boilerplate with batteries 🔋 included. 105 | - https://www.youtube.com/watch?v=b48oOe0VOOc 106 | - https://github.com/V-ed/node-api-template 107 | - https://github.com/nestjsplus?q=&type=&language=&sort= 108 | - https://github.com/juanmesa2097/nestjs-boilerplate 109 | - https://github.com/sgentile/nest-apollo-federation 110 | - https://github.com/0xb4lamx/nestjs-boilerplate-microservice 111 | - https://github.com/danmt/microservices-basics 112 | - https://github.com/ahmetuysal/nest-hackathon-starter 113 | - https://github.com/ArkerLabs/event-sourcing-nestjs-graphql-example 114 | - https://github.com/lnmunhoz/nestjs-graphql-serverless 115 | - https://github.com/joeygoksu/prime-nestjs 116 | - https://github.com/smarlhens/nest-boilerplate 117 | - https://github.com/ArcherGu/fast-vite-nestjs-electron 118 | - https://github.com/kenso312/nestjs-v9-webpack-boilerplate 119 | - https://github.com/razalyalhafiz/orders-payments-microservices - NestJs Eventstore with SSE 120 | 121 | ## Todo 122 | 123 | - https://github.com/the-vampiire/apollo-error-converter 124 | - Health endpoint 125 | -------------------------------------------------------------------------------- /Taskfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH="$PWD/node_modules/.bin":$PATH 3 | set -e 4 | 5 | build() { 6 | set -x 7 | npm run prisma:generate 8 | rm -rf dist 9 | mkdir dist 10 | nest build 11 | mkdir -p dist/prisma/migrations 12 | cp -r prisma/migrations/* dist/prisma/migrations 13 | cp prisma/schema.prisma dist/prisma 14 | mkdir -p dist/node_modules/@prisma/client 15 | cp -r node_modules/@prisma/client/* dist/node_modules/@prisma/client 16 | set +x 17 | } 18 | 19 | git_last_release_tag() { 20 | result="" 21 | rev=$(git rev-list --max-count=1 --tags="v[0-9]*\\.[0-9]*\\.[0-9]*") 22 | if [ -n "$rev" ]; then 23 | result=$(git describe --tags $rev) 24 | fi 25 | if [ -n "$result" ]; then 26 | result=$(git rev-list --max-parents=0 HEAD) 27 | fi 28 | echo $result 29 | } 30 | 31 | commit_lint() { 32 | set -x 33 | from=$(git_last_release_tag) 34 | commitlint --from $from 35 | } 36 | 37 | "$@" 38 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig'); 3 | 4 | module.exports = { 5 | rootDir: 'src', 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | }, 10 | collectCoverage: false, 11 | coverageDirectory: `${__dirname}/coverage`, 12 | coverageReporters: ['lcov', 'text'], 13 | collectCoverageFrom: ['**/*.ts', '!**/*.spec.ts', '!**/*.module.ts'], 14 | testRegex: ['(\\.|/)(test|spec)\\.[jt]sx?$'], 15 | // testMatch: ['/src/**/*.spec.ts'], 16 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 17 | // modulePathIgnorePatterns: ['/dist'], 18 | moduleNameMapper: pathsToModuleNameMapper( 19 | compilerOptions.paths /*, { prefix: '/..' }*/, 20 | ), 21 | globals: { 22 | 'ts-jest': { 23 | diagnostics: false, 24 | isolatedModules: true, 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-typescript-starter", 3 | "private": true, 4 | "version": "0.0.0-dev", 5 | "description": "Nest TypeScript starter repository", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "run-p start:dev prisma:w", 9 | "postinstall": "patch-package && npm run prisma:generate", 10 | "build": "sh Taskfile build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "node -r dotenv-flow/config node_modules/@nestjs/cli/bin/nest.js start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/src/main", 16 | "eslint": "eslint \"{src,test}/**/*.ts\"", 17 | "eslint:fix": "npm run eslint -- --fix", 18 | "eslint:w": "watchexec -w src \"npm run eslint\"", 19 | "tslint:fix": "node node_modules/tslint/bin/tslint -p tsconfig.json --fix", 20 | "lint:fix": "npm run tslint:fix || true && npm run eslint:fix", 21 | "tscheck": "echo tscheck... && tsc --noEmit --incremental false", 22 | "tscheck:w": "npm run tscheck -- --watch", 23 | "test": "npm run eslint && npm run tscheck && npm run test:cov", 24 | "test:r": "jest --runInBand --node-env=test", 25 | "test:w": "npm run test:r -- --watch", 26 | "test:cov": "npm run test:r -- --coverage --verbose", 27 | "test:d": "node --inspect-brk node_modules/jest/bin/jest --runInBand", 28 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 29 | "test:e2e": "node -r dotenv-flow/config node_modules/jest/bin/jest --runInBand --detectOpenHandles --config ./test/jest-e2e.config.js", 30 | "commit": "node node_modules/cz-customizable/standalone.js", 31 | "cmlint": "sh Taskfile commit_lint", 32 | "test:m": "stryker run", 33 | "migrate:save": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js migrate save --experimental", 34 | "migrate:save:preview": "npm run migrate:save -- --preview", 35 | "migrate:up": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js migrate up --experimental --verbose", 36 | "migrate:up:preview": "npm run migrate:up -- --preview", 37 | "migrate:down": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js migrate down --experimental 1", 38 | "prisma:w": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js generate --watch", 39 | "prisma:generate": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js generate", 40 | "prisma:studio": "node -r dotenv-flow/config node_modules/@prisma/cli/build/index.js studio --experimental", 41 | "dev:update": "updtr --out --to latest --test \"npm test:r\"", 42 | "sync-dotenv": "npx sync-dotenv" 43 | }, 44 | "husky": { 45 | "hooks": { 46 | "pre-commit": "precise-commits", 47 | "pre-push": "npm run test", 48 | "commit-msg": "git-branch-is -r \"^(release|master|develop)\" && commitlint -E HUSKY_GIT_PARAMS || echo \"Ignoring commitlint error...\"" 49 | } 50 | }, 51 | "dependencies": { 52 | "@nestjs/common": "^6.11.11", 53 | "@nestjs/config": "^0.4.0", 54 | "@nestjs/core": "^6.11.11", 55 | "@nestjs/graphql": "^6.6.2", 56 | "@nestjs/platform-express": "^6.11.11", 57 | "apollo-server-express": "^2.11.0", 58 | "class-transformer": "^0.2.3", 59 | "class-validator": "^0.11.1", 60 | "graphql": "^14.6.0", 61 | "graphql-fields": "^2.0.3", 62 | "graphql-info-transformer": "0.0.1", 63 | "graphql-tools": "^4.0.7", 64 | "nestjs-pino": "^1.1.3", 65 | "reflect-metadata": "^0.1.13", 66 | "rxjs": "^6.5.4", 67 | "type-graphql": "^0.17.6" 68 | }, 69 | "devDependencies": { 70 | "@nestjs/cli": "^6.14.2", 71 | "@nestjs/schematics": "^6.9.4", 72 | "@nestjs/testing": "^6.11.11", 73 | "@prisma/cli": "^2.0.0-alpha.980", 74 | "@prisma/client": "^2.0.0-alpha.980", 75 | "@stryker-mutator/core": "3.0.2", 76 | "@stryker-mutator/html-reporter": "^3.0.2", 77 | "@stryker-mutator/jest-runner": "^3.0.2", 78 | "@stryker-mutator/typescript": "^3.0.2", 79 | "@types/express": "^4.17.3", 80 | "@types/graphql-fields": "^1.3.2", 81 | "@types/jest": "^25.1.4", 82 | "@types/node": "^13.9.5", 83 | "@types/supertest": "^2.0.8", 84 | "@typescript-eslint/eslint-plugin": "^2.25.0", 85 | "@typescript-eslint/eslint-plugin-tslint": "^2.25.0", 86 | "@typescript-eslint/parser": "^2.25.0", 87 | "cz-customizable": "^6.2.0", 88 | "dotenv-flow": "^3.1.0", 89 | "eslint": "^6.8.0", 90 | "eslint-config-prettier": "^6.10.1", 91 | "eslint-import-resolver-node": "^0.3.3", 92 | "eslint-plugin-import": "^2.20.1", 93 | "eslint-plugin-jest": "^23.8.2", 94 | "eslint-plugin-only-warn": "^1.0.2", 95 | "eslint-plugin-prettier": "^3.1.2", 96 | "eslint-plugin-promise": "^4.2.1", 97 | "eslint-plugin-simple-import-sort": "^5.0.2", 98 | "eslint-plugin-sonarjs": "^0.5.0", 99 | "eslint-plugin-unicorn": "^18.0.1", 100 | "eslint-plugin-wix-editor": "^3.0.0", 101 | "git-branch-is": "^3.1.0", 102 | "husky": "^4.2.3", 103 | "jasmine-mock-factory": "^3.0.0", 104 | "jest": "^25.2.3", 105 | "npm-run-all": "^4.1.5", 106 | "patch-package": "^6.2.1", 107 | "pino-pretty": "^3.6.1", 108 | "precise-commits": "^1.0.2", 109 | "prettier": "^2.0.2", 110 | "simplytyped": "^3.2.3", 111 | "stryker-cli": "^1.0.0", 112 | "supertest": "^4.0.2", 113 | "ts-jest": "^25.2.1", 114 | "ts-loader": "^6.2.2", 115 | "ts-node": "^8.8.1", 116 | "tsconfig-paths": "^3.9.0", 117 | "tslint": "^6.1.0", 118 | "tslint-clean-code": "^0.2.10", 119 | "tslint-consistent-codestyle": "^1.16.0", 120 | "tslint-etc": "^1.10.1", 121 | "tslint-microsoft-contrib": "^6.2.0", 122 | "tslint-sonarts": "^1.9.0", 123 | "typegraphql-prisma": "^0.1.7", 124 | "typescript": "^3.8.3", 125 | "watchexec-bin": "^1.0.0" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /prisma/README.md: -------------------------------------------------------------------------------- 1 | - Migrations can contain hooks https://github.com/prisma/specs/tree/master/lift#hook 2 | -------------------------------------------------------------------------------- /prisma/migrations/20200301180958-initial/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200301180958-initial` 2 | 3 | This migration has been generated by roman <<>> at 3/1/2020, 6:09:58 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "quaint"."User" ( 10 | "createdAt" DATE NOT NULL DEFAULT '1970-01-01 00:00:00' , 11 | "email" TEXT NOT NULL DEFAULT '' , 12 | "id" TEXT NOT NULL , 13 | "name" TEXT , 14 | PRIMARY KEY ("id") 15 | ) 16 | 17 | CREATE TABLE "quaint"."Post" ( 18 | "author" TEXT NOT NULL , 19 | "createdAt" DATE NOT NULL DEFAULT '1970-01-01 00:00:00' , 20 | "id" TEXT NOT NULL , 21 | "title" TEXT NOT NULL DEFAULT '' , 22 | "updatedAt" DATE NOT NULL DEFAULT '1970-01-01 00:00:00' , 23 | PRIMARY KEY ("id"),FOREIGN KEY ("author") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE 24 | ) 25 | 26 | CREATE UNIQUE INDEX "quaint"."User.email" ON "User"("email") 27 | ``` 28 | 29 | ## Changes 30 | 31 | ```diff 32 | diff --git schema.prisma schema.prisma 33 | migration ..20200301180958-initial 34 | --- datamodel.dml 35 | +++ datamodel.dml 36 | @@ -1,0 +1,32 @@ 37 | +// This is Prisma schema file, 38 | +// learn more about it in the docs: https://pris.ly/d/prisma-schema 39 | + 40 | +datasource db { 41 | + provider = "sqlite" 42 | + url = env("DATABASE_URL") 43 | +} 44 | + 45 | +generator client { 46 | + provider = "prisma-client-js" 47 | +} 48 | + 49 | +generator typegraphql { 50 | + provider = "node_modules/typegraphql-prisma/generator.js" 51 | + output = "../@generated/type-graphql" 52 | +} 53 | + 54 | +model User { 55 | + id String @id @default(cuid()) 56 | + createdAt DateTime @default(now()) 57 | + email String @unique 58 | + name String? 59 | + posts Post[] 60 | +} 61 | + 62 | +model Post { 63 | + id String @id @default(cuid()) 64 | + createdAt DateTime @default(now()) 65 | + updatedAt DateTime @updatedAt 66 | + author User 67 | + title String 68 | +} 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /prisma/migrations/20200301180958-initial/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "sqlite" 6 | url = "***" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | generator typegraphql { 14 | provider = "node_modules/typegraphql-prisma/generator.js" 15 | output = "../@generated/type-graphql" 16 | } 17 | 18 | model User { 19 | id String @id @default(cuid()) 20 | createdAt DateTime @default(now()) 21 | email String @unique 22 | name String? 23 | posts Post[] 24 | } 25 | 26 | model Post { 27 | id String @id @default(cuid()) 28 | createdAt DateTime @default(now()) 29 | updatedAt DateTime @updatedAt 30 | author User 31 | title String 32 | } 33 | -------------------------------------------------------------------------------- /prisma/migrations/20200301180958-initial/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateSource", 6 | "source": "db" 7 | }, 8 | { 9 | "tag": "CreateArgument", 10 | "location": { 11 | "tag": "Source", 12 | "source": "db" 13 | }, 14 | "argument": "provider", 15 | "value": "\"sqlite\"" 16 | }, 17 | { 18 | "tag": "CreateArgument", 19 | "location": { 20 | "tag": "Source", 21 | "source": "db" 22 | }, 23 | "argument": "url", 24 | "value": "env(\"DATABASE_URL\")" 25 | }, 26 | { 27 | "tag": "CreateModel", 28 | "model": "User" 29 | }, 30 | { 31 | "tag": "CreateField", 32 | "model": "User", 33 | "field": "id", 34 | "type": "String", 35 | "arity": "Required" 36 | }, 37 | { 38 | "tag": "CreateDirective", 39 | "location": { 40 | "path": { 41 | "tag": "Field", 42 | "model": "User", 43 | "field": "id" 44 | }, 45 | "directive": "id" 46 | } 47 | }, 48 | { 49 | "tag": "CreateDirective", 50 | "location": { 51 | "path": { 52 | "tag": "Field", 53 | "model": "User", 54 | "field": "id" 55 | }, 56 | "directive": "default" 57 | } 58 | }, 59 | { 60 | "tag": "CreateArgument", 61 | "location": { 62 | "tag": "Directive", 63 | "path": { 64 | "tag": "Field", 65 | "model": "User", 66 | "field": "id" 67 | }, 68 | "directive": "default" 69 | }, 70 | "argument": "", 71 | "value": "cuid()" 72 | }, 73 | { 74 | "tag": "CreateField", 75 | "model": "User", 76 | "field": "createdAt", 77 | "type": "DateTime", 78 | "arity": "Required" 79 | }, 80 | { 81 | "tag": "CreateDirective", 82 | "location": { 83 | "path": { 84 | "tag": "Field", 85 | "model": "User", 86 | "field": "createdAt" 87 | }, 88 | "directive": "default" 89 | } 90 | }, 91 | { 92 | "tag": "CreateArgument", 93 | "location": { 94 | "tag": "Directive", 95 | "path": { 96 | "tag": "Field", 97 | "model": "User", 98 | "field": "createdAt" 99 | }, 100 | "directive": "default" 101 | }, 102 | "argument": "", 103 | "value": "now()" 104 | }, 105 | { 106 | "tag": "CreateField", 107 | "model": "User", 108 | "field": "email", 109 | "type": "String", 110 | "arity": "Required" 111 | }, 112 | { 113 | "tag": "CreateDirective", 114 | "location": { 115 | "path": { 116 | "tag": "Field", 117 | "model": "User", 118 | "field": "email" 119 | }, 120 | "directive": "unique" 121 | } 122 | }, 123 | { 124 | "tag": "CreateField", 125 | "model": "User", 126 | "field": "name", 127 | "type": "String", 128 | "arity": "Optional" 129 | }, 130 | { 131 | "tag": "CreateField", 132 | "model": "User", 133 | "field": "posts", 134 | "type": "Post", 135 | "arity": "List" 136 | }, 137 | { 138 | "tag": "CreateModel", 139 | "model": "Post" 140 | }, 141 | { 142 | "tag": "CreateField", 143 | "model": "Post", 144 | "field": "id", 145 | "type": "String", 146 | "arity": "Required" 147 | }, 148 | { 149 | "tag": "CreateDirective", 150 | "location": { 151 | "path": { 152 | "tag": "Field", 153 | "model": "Post", 154 | "field": "id" 155 | }, 156 | "directive": "id" 157 | } 158 | }, 159 | { 160 | "tag": "CreateDirective", 161 | "location": { 162 | "path": { 163 | "tag": "Field", 164 | "model": "Post", 165 | "field": "id" 166 | }, 167 | "directive": "default" 168 | } 169 | }, 170 | { 171 | "tag": "CreateArgument", 172 | "location": { 173 | "tag": "Directive", 174 | "path": { 175 | "tag": "Field", 176 | "model": "Post", 177 | "field": "id" 178 | }, 179 | "directive": "default" 180 | }, 181 | "argument": "", 182 | "value": "cuid()" 183 | }, 184 | { 185 | "tag": "CreateField", 186 | "model": "Post", 187 | "field": "createdAt", 188 | "type": "DateTime", 189 | "arity": "Required" 190 | }, 191 | { 192 | "tag": "CreateDirective", 193 | "location": { 194 | "path": { 195 | "tag": "Field", 196 | "model": "Post", 197 | "field": "createdAt" 198 | }, 199 | "directive": "default" 200 | } 201 | }, 202 | { 203 | "tag": "CreateArgument", 204 | "location": { 205 | "tag": "Directive", 206 | "path": { 207 | "tag": "Field", 208 | "model": "Post", 209 | "field": "createdAt" 210 | }, 211 | "directive": "default" 212 | }, 213 | "argument": "", 214 | "value": "now()" 215 | }, 216 | { 217 | "tag": "CreateField", 218 | "model": "Post", 219 | "field": "updatedAt", 220 | "type": "DateTime", 221 | "arity": "Required" 222 | }, 223 | { 224 | "tag": "CreateDirective", 225 | "location": { 226 | "path": { 227 | "tag": "Field", 228 | "model": "Post", 229 | "field": "updatedAt" 230 | }, 231 | "directive": "updatedAt" 232 | } 233 | }, 234 | { 235 | "tag": "CreateField", 236 | "model": "Post", 237 | "field": "author", 238 | "type": "User", 239 | "arity": "Required" 240 | }, 241 | { 242 | "tag": "CreateField", 243 | "model": "Post", 244 | "field": "title", 245 | "type": "String", 246 | "arity": "Required" 247 | } 248 | ] 249 | } -------------------------------------------------------------------------------- /prisma/migrations/20200317005941-count-posts/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200317005941-count-posts` 2 | 3 | This migration has been generated by roman <<>> at 3/17/2020, 12:59:41 AM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | PRAGMA foreign_keys=OFF; 10 | 11 | CREATE TABLE "quaint"."new_User" ( 12 | "countPosts" INTEGER NOT NULL DEFAULT 0 , 13 | "createdAt" DATE NOT NULL DEFAULT '1970-01-01 00:00:00' , 14 | "email" TEXT NOT NULL DEFAULT '' , 15 | "id" TEXT NOT NULL , 16 | "name" TEXT , 17 | PRIMARY KEY ("id") 18 | ) 19 | 20 | INSERT INTO "quaint"."new_User" ("createdAt", "email", "id", "name") SELECT "createdAt", "email", "id", "name" FROM "quaint"."User" 21 | 22 | DROP TABLE "quaint"."User"; 23 | 24 | ALTER TABLE "quaint"."new_User" RENAME TO "User"; 25 | 26 | CREATE UNIQUE INDEX "quaint"."User.email" ON "User"("email") 27 | 28 | PRAGMA "quaint".foreign_key_check; 29 | 30 | PRAGMA foreign_keys=ON; 31 | ``` 32 | 33 | ## Changes 34 | 35 | ```diff 36 | diff --git schema.prisma schema.prisma 37 | migration 20200301180958-initial..20200317005941-count-posts 38 | --- datamodel.dml 39 | +++ datamodel.dml 40 | @@ -2,9 +2,9 @@ 41 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 42 | datasource db { 43 | provider = "sqlite" 44 | - url = "***" 45 | + url = env("DATABASE_URL") 46 | } 47 | generator client { 48 | provider = "prisma-client-js" 49 | @@ -19,9 +19,10 @@ 50 | id String @id @default(cuid()) 51 | createdAt DateTime @default(now()) 52 | email String @unique 53 | name String? 54 | - posts Post[] 55 | + posts Post[] 56 | + countPosts Int @default(0) 57 | } 58 | model Post { 59 | id String @id @default(cuid()) 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /prisma/migrations/20200317005941-count-posts/_after.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { PrismaClient } = require('@prisma/client'); 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | prisma.raw(` 8 | UPDATE User 9 | SET countPosts = (SELECT COUNT() FROM Post WHERE Post.author = User.id) 10 | `); 11 | 12 | prisma.raw(` 13 | CREATE TRIGGER updateUserPostCount AFTER INSERT ON Post FOR EACH ROW 14 | BEGIN 15 | UPDATE User 16 | SET postCount = (SELECT COUNT() FROM Post WHERE Post.author = new.author) 17 | WHERE User.id = new.author; 18 | END; 19 | `); 20 | -------------------------------------------------------------------------------- /prisma/migrations/20200317005941-count-posts/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "sqlite" 6 | url = "***" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | generator typegraphql { 14 | provider = "node_modules/typegraphql-prisma/generator.js" 15 | output = "../@generated/type-graphql" 16 | } 17 | 18 | model User { 19 | id String @id @default(cuid()) 20 | createdAt DateTime @default(now()) 21 | email String @unique 22 | name String? 23 | posts Post[] 24 | countPosts Int @default(0) 25 | } 26 | 27 | model Post { 28 | id String @id @default(cuid()) 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | author User 32 | title String 33 | } 34 | -------------------------------------------------------------------------------- /prisma/migrations/20200317005941-count-posts/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateField", 6 | "model": "User", 7 | "field": "countPosts", 8 | "type": "Int", 9 | "arity": "Required" 10 | }, 11 | { 12 | "tag": "CreateDirective", 13 | "location": { 14 | "path": { 15 | "tag": "Field", 16 | "model": "User", 17 | "field": "countPosts" 18 | }, 19 | "directive": "default" 20 | } 21 | }, 22 | { 23 | "tag": "CreateArgument", 24 | "location": { 25 | "tag": "Directive", 26 | "path": { 27 | "tag": "Field", 28 | "model": "User", 29 | "field": "countPosts" 30 | }, 31 | "directive": "default" 32 | }, 33 | "argument": "", 34 | "value": "0" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /prisma/migrations/20200327000541-category/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200327000541-category` 2 | 3 | This migration has been generated by roman <<>> at 3/27/2020, 12:05:41 AM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "quaint"."Category" ( 10 | "id" TEXT NOT NULL , 11 | "name" TEXT NOT NULL DEFAULT '' , 12 | PRIMARY KEY ("id") 13 | ) 14 | 15 | CREATE TABLE "quaint"."_CategoryToPost" ( 16 | "A" TEXT NOT NULL , 17 | "B" TEXT NOT NULL ,FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE, 18 | FOREIGN KEY ("B") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE 19 | ) 20 | 21 | CREATE UNIQUE INDEX "quaint"."_CategoryToPost_AB_unique" ON "_CategoryToPost"("A","B") 22 | 23 | CREATE INDEX "quaint"."_CategoryToPost_B_index" ON "_CategoryToPost"("B") 24 | ``` 25 | 26 | ## Changes 27 | 28 | ```diff 29 | diff --git schema.prisma schema.prisma 30 | migration 20200317005941-count-posts..20200327000541-category 31 | --- datamodel.dml 32 | +++ datamodel.dml 33 | @@ -2,9 +2,9 @@ 34 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 35 | datasource db { 36 | provider = "sqlite" 37 | - url = "***" 38 | + url = env("DATABASE_URL") 39 | } 40 | generator client { 41 | provider = "prisma-client-js" 42 | @@ -15,19 +15,26 @@ 43 | output = "../@generated/type-graphql" 44 | } 45 | model User { 46 | - id String @id @default(cuid()) 47 | - createdAt DateTime @default(now()) 48 | - email String @unique 49 | - name String? 50 | - posts Post[] 51 | - countPosts Int @default(0) 52 | + id String @id @default(cuid()) 53 | + createdAt DateTime @default(now()) 54 | + email String @unique 55 | + name String? 56 | + posts Post[] 57 | + countPosts Int @default(0) 58 | } 59 | model Post { 60 | - id String @id @default(cuid()) 61 | - createdAt DateTime @default(now()) 62 | - updatedAt DateTime @updatedAt 63 | - author User 64 | - title String 65 | + id String @id @default(cuid()) 66 | + createdAt DateTime @default(now()) 67 | + updatedAt DateTime @updatedAt 68 | + author User 69 | + title String 70 | + categories Category[] 71 | } 72 | + 73 | +model Category { 74 | + id String @id @default(cuid()) 75 | + name String 76 | + posts Post[] 77 | +} 78 | ``` 79 | 80 | 81 | -------------------------------------------------------------------------------- /prisma/migrations/20200327000541-category/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "sqlite" 6 | url = "***" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | generator typegraphql { 14 | provider = "node_modules/typegraphql-prisma/generator.js" 15 | output = "../@generated/type-graphql" 16 | } 17 | 18 | model User { 19 | id String @id @default(cuid()) 20 | createdAt DateTime @default(now()) 21 | email String @unique 22 | name String? 23 | posts Post[] 24 | countPosts Int @default(0) 25 | } 26 | 27 | model Post { 28 | id String @id @default(cuid()) 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | author User 32 | title String 33 | categories Category[] 34 | } 35 | 36 | model Category { 37 | id String @id @default(cuid()) 38 | name String 39 | posts Post[] 40 | } 41 | -------------------------------------------------------------------------------- /prisma/migrations/20200327000541-category/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateModel", 6 | "model": "Category" 7 | }, 8 | { 9 | "tag": "CreateField", 10 | "model": "Category", 11 | "field": "id", 12 | "type": "String", 13 | "arity": "Required" 14 | }, 15 | { 16 | "tag": "CreateDirective", 17 | "location": { 18 | "path": { 19 | "tag": "Field", 20 | "model": "Category", 21 | "field": "id" 22 | }, 23 | "directive": "id" 24 | } 25 | }, 26 | { 27 | "tag": "CreateDirective", 28 | "location": { 29 | "path": { 30 | "tag": "Field", 31 | "model": "Category", 32 | "field": "id" 33 | }, 34 | "directive": "default" 35 | } 36 | }, 37 | { 38 | "tag": "CreateArgument", 39 | "location": { 40 | "tag": "Directive", 41 | "path": { 42 | "tag": "Field", 43 | "model": "Category", 44 | "field": "id" 45 | }, 46 | "directive": "default" 47 | }, 48 | "argument": "", 49 | "value": "cuid()" 50 | }, 51 | { 52 | "tag": "CreateField", 53 | "model": "Category", 54 | "field": "name", 55 | "type": "String", 56 | "arity": "Required" 57 | }, 58 | { 59 | "tag": "CreateField", 60 | "model": "Category", 61 | "field": "posts", 62 | "type": "Post", 63 | "arity": "List" 64 | }, 65 | { 66 | "tag": "CreateField", 67 | "model": "Post", 68 | "field": "categories", 69 | "type": "Category", 70 | "arity": "List" 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /prisma/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # IF THERE'S A GIT CONFLICT IN THIS FILE, DON'T SOLVE IT MANUALLY! 2 | # INSTEAD EXECUTE `prisma2 migrate fix` 3 | # Prisma Migrate lockfile v1 4 | # Read more about conflict resolution here: TODO 5 | 6 | 20200301180958-initial 7 | 20200317005941-count-posts 8 | 20200327000541-category -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "sqlite" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | generator typegraphql { 14 | provider = "node_modules/typegraphql-prisma/generator.js" 15 | output = "../@generated/type-graphql" 16 | } 17 | 18 | model User { 19 | id String @id @default(cuid()) 20 | createdAt DateTime @default(now()) 21 | email String @unique 22 | name String? 23 | posts Post[] 24 | countPosts Int @default(0) 25 | } 26 | 27 | model Post { 28 | id String @id @default(cuid()) 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | author User 32 | title String 33 | categories Category[] 34 | } 35 | 36 | model Category { 37 | id String @id @default(cuid()) 38 | name String 39 | posts Post[] 40 | } 41 | -------------------------------------------------------------------------------- /prisma/schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- File generated with SQLiteStudio v3.2.1 on Sat Feb 29 20:12:47 2020 3 | -- 4 | -- Text encoding used: System 5 | -- 6 | PRAGMA foreign_keys = off; 7 | BEGIN TRANSACTION; 8 | 9 | -- Table: User 10 | CREATE TABLE User ( 11 | createdAt DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | email TEXT NOT NULL DEFAULT '', 13 | id TEXT NOT NULL, 14 | name TEXT, 15 | PRIMARY KEY (id) 16 | ); 17 | 18 | CREATE TABLE Post ( 19 | author TEXT NOT NULL, 20 | createdAt DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | id TEXT NOT NULL, 22 | title TEXT NOT NULL DEFAULT '', 23 | updatedAt DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, 24 | PRIMARY KEY (id), 25 | FOREIGN KEY (author) 26 | REFERENCES User (id) ON DELETE CASCADE 27 | ON UPDATE CASCADE 28 | ); 29 | 30 | COMMIT TRANSACTION; 31 | PRAGMA foreign_keys = on; 32 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | async function main() { 7 | console.log('Seeding...'); 8 | await prisma.connect(); 9 | 10 | const user1 = await prisma.user.create({ 11 | data: { 12 | email: 'lisa@simpson.com', 13 | name: 'Lisa Simpson', 14 | }, 15 | }); 16 | const user2 = await prisma.user.create({ 17 | data: { 18 | email: 'bart@simpson.com', 19 | name: 'Bart Simpson', 20 | }, 21 | }); 22 | 23 | console.log({ user1, user2 }); 24 | } 25 | 26 | main() 27 | .catch(e => console.error(e)) 28 | .finally(async () => { 29 | await prisma.disconnect(); 30 | }); 31 | -------------------------------------------------------------------------------- /prisma/select-playground.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | const prisma = new PrismaClient({ log: ['query'] }); 5 | 6 | async function main() { 7 | const categories = await prisma.category.findMany({ 8 | where: { id: 'ck896v57l0001hgwz55r9wy6m' }, 9 | select: { name: true, posts: { select: { id: true, title: true, author: true } } }, 10 | }); 11 | console.log('categories', JSON.stringify(categories, null, 2)); 12 | await prisma.connect(); 13 | } 14 | 15 | main() 16 | .catch((e) => console.error(e)) 17 | .finally(async () => { 18 | await prisma.disconnect(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | export const config = () => ({ 2 | name: 'Application', 3 | get port() { 4 | return process.env.PORT || 3000; 5 | }, 6 | get production() { 7 | return process.env.NODE_ENV === 'production'; 8 | }, 9 | }); 10 | 11 | export type AppConfig = Readonly>; 12 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { GraphQLModule } from '@nestjs/graphql'; 4 | import { LoggerModule } from 'nestjs-pino'; 5 | 6 | import { config } from './app.config'; 7 | import { AppResolver } from './app.resolver'; 8 | import { AppService } from './app.service'; 9 | import { PrismaModule } from './prisma/prisma.module'; 10 | import { PrismaService } from './prisma/prisma.service'; 11 | import { UserModule } from './user/user.module'; 12 | 13 | export async function graphqlModuleFactory(prismaService: PrismaService) { 14 | return { 15 | tracing: false, 16 | autoSchemaFile: '~schema.gql', 17 | context: () => ({ prisma: prismaService }), 18 | // formatError: null as any, 19 | }; 20 | } 21 | 22 | @Module({ 23 | imports: [ 24 | ConfigModule.forRoot({ 25 | isGlobal: true, 26 | envFilePath: ['.env'], 27 | load: [config], 28 | }), 29 | LoggerModule.forRootAsync({ 30 | inject: [ConfigService], 31 | useFactory: (config: ConfigService) => { 32 | return { 33 | pinoHttp: { 34 | useLevel: 'trace', 35 | level: config.get('production') ? 'warn' : 'info', 36 | prettyPrint: !config.get('production'), 37 | }, 38 | }; 39 | }, 40 | }), 41 | UserModule, 42 | PrismaModule, 43 | GraphQLModule.forRootAsync({ 44 | imports: [PrismaModule], 45 | inject: [PrismaService], 46 | useFactory: graphqlModuleFactory, 47 | }), 48 | ], 49 | providers: [ 50 | { provide: AppService, useClass: AppService }, 51 | { provide: AppResolver, useClass: AppResolver }, 52 | ], 53 | }) 54 | export class AppModule {} 55 | -------------------------------------------------------------------------------- /src/app.resolver.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@nestjs/config'; 2 | import { Query, ResolveProperty, Resolver } from '@nestjs/graphql'; 3 | import { PinoLogger } from 'nestjs-pino'; 4 | 5 | import { PrismaService } from './prisma/prisma.service'; 6 | import { AppModel } from './types/app.model'; 7 | 8 | @Resolver(() => AppModel) 9 | export class AppResolver { 10 | constructor( 11 | private readonly logger: PinoLogger, 12 | private readonly config: ConfigService, 13 | private readonly prisma: PrismaService, 14 | ) { 15 | this.logger.setContext(this.constructor.name); 16 | } 17 | 18 | @Query(() => AppModel) 19 | app() { 20 | return { 21 | version: null, 22 | }; 23 | } 24 | 25 | @ResolveProperty(() => String, { name: 'version' }) 26 | version() { 27 | return '0.0.1'; 28 | } 29 | 30 | @ResolveProperty(() => String) 31 | async databaseHealth() { 32 | let result = 'ok'; 33 | try { 34 | await this.prisma.raw(`select 1 from "User" limit 1`); 35 | } catch (e) { 36 | result = `not ok (${e.message})`; 37 | } 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppService', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | providers: [AppService], 11 | }).compile(); 12 | }); 13 | 14 | describe('getHello', () => { 15 | it('should return Hello World!', () => { 16 | const service = app.get(AppService); 17 | expect(service.getHello()).toBe('Hello World!'); 18 | }); 19 | }); 20 | }); 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/app_modules/nestjs-prisma-select/index.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | import { infoToPhotonSelect } from 'graphql-info-transformer'; 3 | 4 | /** 5 | * Transform GraphQL's info into an object that can be consumed by Prisma's client 6 | */ 7 | export const Select = createParamDecorator((data, [root, args, context, info]) => { 8 | return infoToPhotonSelect(info); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app_modules/nestjs-select-fields/index.ts: -------------------------------------------------------------------------------- 1 | export { SelectFields } from './select-fields.decorator'; 2 | -------------------------------------------------------------------------------- /src/app_modules/nestjs-select-fields/select-fields.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | import { 3 | buildSchema, 4 | ExecutionResult, 5 | graphql, 6 | GraphQLFieldResolver, 7 | GraphQLResolveInfo, 8 | } from 'graphql'; 9 | 10 | jest.mock('@nestjs/common', () => { 11 | const originalModule = jest.requireActual('@nestjs/common'); 12 | return { 13 | __esModule: true, 14 | ...originalModule, 15 | createParamDecorator: jest.fn(), 16 | }; 17 | }); 18 | 19 | let info: GraphQLResolveInfo | undefined; 20 | const schema = buildSchema(` 21 | type Query { 22 | ok: Boolean 23 | user: User 24 | } 25 | 26 | type User { 27 | id: String 28 | posts: [Post] 29 | } 30 | 31 | type Post { 32 | id: Int 33 | title: String 34 | } 35 | 36 | schema { 37 | query: Query 38 | } 39 | `); 40 | 41 | async function executeGraphQL(query) { 42 | const resolver: GraphQLFieldResolver = ( 43 | source, 44 | args, 45 | context, 46 | i: GraphQLResolveInfo, 47 | ) => { 48 | info = i; 49 | }; 50 | const graphqlArgs = { 51 | source: query, 52 | schema: schema, 53 | fieldResolver: resolver, 54 | }; 55 | const response: ExecutionResult = await graphql(graphqlArgs); 56 | (createParamDecorator as jest.Mock).mockImplementation((callback) => { 57 | return callback.bind(undefined, undefined, [undefined, undefined, undefined, info]); 58 | }); 59 | const { SelectFields } = await import('./select-fields.decorator'); 60 | return SelectFields; 61 | } 62 | 63 | it('select id', async () => { 64 | const SelectFields = await executeGraphQL(`query { user { id } }`); 65 | const result = SelectFields(); 66 | expect(result).toEqual({ id: true }); 67 | }); 68 | 69 | it('nested fields should not be selected', async () => { 70 | const SelectFields = await executeGraphQL(`query { user { id, posts { title } } }`); 71 | const result = SelectFields(); 72 | expect(result).toEqual({ id: true }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/app_modules/nestjs-select-fields/select-fields.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | import graphqlFields from 'graphql-fields'; 3 | 4 | export const SelectFields = createParamDecorator((data, [root, args, context, info]) => { 5 | const fields = graphqlFields(info); 6 | return Object.keys(fields).reduce((result, field) => { 7 | result[field] = true; 8 | return result; 9 | }, {}); 10 | }); 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { Logger } from 'nestjs-pino'; 5 | 6 | import { AppModule } from './app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule, { logger: false }); 10 | const logger = app.get(Logger); 11 | const config = app.get(ConfigService); 12 | app.useLogger(logger); 13 | app.useGlobalPipes( 14 | new ValidationPipe({ 15 | validationError: { 16 | target: false, 17 | }, 18 | }), 19 | ); 20 | const port = config.get('port', 3000); 21 | await app.listen(port); 22 | logger.log('Server is launched on port %o', 'bootstrap', port); 23 | } 24 | bootstrap(); 25 | -------------------------------------------------------------------------------- /src/post/models/post.ts: -------------------------------------------------------------------------------- 1 | import { Post as PostModel } from '@generated/type-graphql/models/Post'; 2 | import { Field, ID, ObjectType } from 'type-graphql'; 3 | 4 | @ObjectType() 5 | export class Post extends PostModel { 6 | @Field((_type) => ID) 7 | id!: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from './prisma.service'; 4 | 5 | @Module({ 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { PrismaService } from './prisma.service'; 4 | 5 | describe('PrismaService', () => { 6 | let service: PrismaService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [PrismaService], 11 | }).compile(); 12 | 13 | service = module.get(PrismaService); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { 6 | constructor() { 7 | super({ 8 | errorFormat: 'minimal', 9 | log: ['query'], 10 | }); 11 | } 12 | 13 | async onModuleInit() { 14 | await this.connect(); 15 | } 16 | 17 | async onModuleDestroy() { 18 | await this.disconnect(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/types/app.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType('App') 4 | export class AppModel { 5 | @Field(() => String, { nullable: false }) 6 | version: string; 7 | 8 | @Field(() => String, { nullable: false }) 9 | databaseHealth: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { graphqlModuleFactory } from '../app.module'; 2 | 3 | export type Await = T extends { 4 | then(onfulfilled?: (value: infer U) => unknown): unknown; 5 | } 6 | ? U 7 | : T; 8 | 9 | export type GraphQLContext = ReturnType>['context']>; 10 | -------------------------------------------------------------------------------- /src/user/create-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class CreateUserGuard implements CanActivate { 6 | canActivate(context: ExecutionContext): boolean | Promise | Observable { 7 | // Validate 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/user/models/user-create-input.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | export class UserCreateInput { 6 | @IsEmail() 7 | @Field((_type) => String, { nullable: false }) 8 | email: string; 9 | 10 | @IsNotEmpty() 11 | @MinLength(3) 12 | @Field((_type) => String, { nullable: false }) 13 | name: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/user/models/user.ts: -------------------------------------------------------------------------------- 1 | import { User as UserModel } from '@generated/type-graphql/models/User'; 2 | import { Field, ID, ObjectType } from 'type-graphql'; 3 | 4 | @ObjectType({}) 5 | export class User extends UserModel { 6 | @Field((_type) => ID, { 7 | nullable: false, 8 | description: undefined, 9 | }) 10 | id: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | import { UserRepository } from './user.repository'; 5 | import { UserResolver } from './user.resolver'; 6 | import { UserService } from './user.service'; 7 | 8 | @Module({ 9 | providers: [UserService, UserResolver, UserRepository, PrismaService], 10 | }) 11 | export class UserModule {} 12 | -------------------------------------------------------------------------------- /src/user/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from '@prisma/client'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | 6 | @Injectable() 7 | export class UserRepository { 8 | constructor(private readonly prisma: PrismaService) {} 9 | 10 | async randomUser() { 11 | const [result] = await this.prisma.raw( 12 | `SELECT * FROM "User" ORDER BY random() LIMIT 1`, 13 | ); 14 | return result; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { FindManyPostArgs } from '@generated/type-graphql/resolvers/crud/Post/args/FindManyPostArgs'; 2 | import { FindManyUserArgs } from '@generated/type-graphql/resolvers/crud/User/args/FindManyUserArgs'; 3 | import { UserWhereUniqueInput } from '@generated/type-graphql/resolvers/inputs/UserWhereUniqueInput'; 4 | import { UseGuards, UsePipes } from '@nestjs/common'; 5 | import { Args, Context, Mutation, Parent, Query, ResolveProperty, Resolver } from '@nestjs/graphql'; 6 | import { UserSelect } from '@prisma/client'; 7 | 8 | import { Select } from '~app_modules/nestjs-prisma-select'; 9 | 10 | import { Post } from '../post/models/post'; 11 | import { GraphQLContext } from '../types'; 12 | import { CreateUserGuard } from './create-user.guard'; 13 | import { User } from './models/user'; 14 | import { UserCreateInput } from './models/user-create-input'; 15 | import { UserService } from './user.service'; 16 | 17 | @Resolver(() => User) 18 | export class UserResolver { 19 | constructor(private readonly userService: UserService) {} 20 | 21 | @Query(() => User, { nullable: true }) 22 | async randomUser() { 23 | return this.userService.findOneRandom(); 24 | } 25 | 26 | @Query(() => User, { nullable: true }) 27 | async findOneUser( 28 | @Context() context: GraphQLContext, 29 | @Args('where') where: UserWhereUniqueInput, 30 | @Select() select: UserSelect, 31 | ) { 32 | return context.prisma.user.findOne({ 33 | where, 34 | select, 35 | }); 36 | } 37 | 38 | @ResolveProperty(() => [Post]) 39 | async posts(@Parent() user: User, @Args() args: FindManyPostArgs) { 40 | return user.posts || []; 41 | } 42 | 43 | @Query(() => [User]) 44 | async findManyUser(@Args() args: FindManyUserArgs) { 45 | return this.userService.findMany(args); 46 | } 47 | 48 | @Mutation(() => User) 49 | @UseGuards(CreateUserGuard) 50 | @UsePipes() 51 | async createUser(@Args('data') data: UserCreateInput) { 52 | return this.userService.create(data); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { FindManyUserArgs } from '@generated/type-graphql/resolvers/crud/User/args/FindManyUserArgs'; 2 | import { UserCreateInput } from '@generated/type-graphql/resolvers/inputs/UserCreateInput'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | import { PrismaService } from '../prisma/prisma.service'; 6 | import { UserRepository } from './user.repository'; 7 | 8 | @Injectable() 9 | export class UserService { 10 | constructor( 11 | private readonly userRepository: UserRepository, 12 | private readonly prisma: PrismaService, 13 | ) {} 14 | 15 | async findOneRandom() { 16 | return this.userRepository.randomUser(); 17 | } 18 | 19 | async findMany(args: FindManyUserArgs) { 20 | return this.prisma.user.findMany(args); 21 | } 22 | 23 | async create(data: UserCreateInput) { 24 | return this.prisma.user.create({ data }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | mutate: [ 4 | 'src/**/*.{ts,tsx}', 5 | '!src/**/*.spec.{ts,tsx}', 6 | '!src/**/*.module.ts', 7 | '!src/examples/**/*', 8 | '!src/**/testing/**/*', 9 | ], 10 | files: [ 11 | 'jest.config.js', 12 | 'src/**/*.tsx', 13 | 'src/**/*.ts', 14 | 'src/**/*.json', 15 | 'package.json', 16 | 'tsconfig.json', 17 | ], 18 | mutator: 'typescript', 19 | packageManager: 'npm', 20 | reporters: ['progress', 'html', 'clear-text'], 21 | testRunner: 'jest', 22 | transpilers: [], 23 | coverageAnalysis: 'off', 24 | tsconfigFile: 'tsconfig.json', 25 | tempDirName: 'stryker-tmp', 26 | htmlReporter: { 27 | baseDir: 'coverage/mutation', 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /test/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Server } from 'http'; 4 | import request from 'supertest'; 5 | 6 | import { AppModule } from './../src/app.module'; 7 | 8 | describe('AppController (e2e)', () => { 9 | let app: INestApplication; 10 | let server: Server; 11 | 12 | beforeAll(async () => { 13 | const moduleFixture = await Test.createTestingModule({ 14 | imports: [AppModule], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication(); 18 | await app.init(); 19 | server = app.getHttpServer(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await app.close(); 24 | }); 25 | 26 | it('graphql app version', async (done) => { 27 | const result = await request(server) 28 | .post('/graphql') 29 | .send({ query: `{ app { version } }` }); 30 | expect(result.body.data).toEqual({ app: { version: '0.0.1' } }); 31 | done(); 32 | }); 33 | 34 | it('/ (GET)', async (done) => { 35 | await request(server).get('/').expect(404); 36 | done(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/jest-e2e.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('../tsconfig'); 3 | 4 | module.exports = { 5 | moduleFileExtensions: ['js', 'json', 'ts'], 6 | rootDir: '.', 7 | testEnvironment: 'node', 8 | testRegex: '.(e2e-spec.ts|spec.ts)$', 9 | transform: { 10 | '^.+\\.(t|j)s$': 'ts-jest', 11 | }, 12 | moduleNameMapper: pathsToModuleNameMapper( 13 | compilerOptions.paths /*, { prefix: '/..' }*/, 14 | ), 15 | }; 16 | -------------------------------------------------------------------------------- /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 | "target": "es2020", 5 | "importHelpers": true, 6 | "declaration": false, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "incremental": true, 15 | "outDir": "./dist", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@generated/*": ["@generated/*"], 19 | "~app_modules/*": ["src/app_modules/*"] 20 | } 21 | }, 22 | "include": ["src/**/*.ts", "test/**/*.ts", "@generated/*", "prisma/*.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-etc", 5 | "tslint-microsoft-contrib", 6 | "tslint-clean-code", 7 | "tslint-consistent-codestyle" 8 | ], 9 | "rulesDirectory": [ 10 | "node_modules/tslint/lib/rules", 11 | "node_modules/tslint-microsoft-contrib", 12 | "node_modules/tslint-clean-code/dist/src", 13 | "node_modules/tslint-consistent-codestyle" 14 | ], 15 | "rules": { 16 | "no-implicit-dependencies": [true, ["@generated/type-graphql", "~app_modules"]], 17 | "no-submodule-imports": false, 18 | "no-unnecessary-class": false, 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": "fields-first" 23 | } 24 | ], 25 | "no-unused-declaration": { 26 | "options": [ 27 | { 28 | "declarations": true, 29 | "ignored": {}, 30 | "imports": true 31 | } 32 | ], 33 | "severity": "warn" 34 | }, 35 | "triple-equals": [true, "allow-undefined-check"], 36 | "no-use-before-declare": false, 37 | "no-function-expression": false, 38 | "member-access": false, 39 | "ordered-imports": false, 40 | "quotemark": false, 41 | "no-var-keyword": false, 42 | "object-literal-sort-keys": false, 43 | "no-console": false, 44 | "arrow-parens": false, 45 | "max-line-length": false, 46 | "object-literal-key-quotes": false, 47 | "no-shadowed-variable": false, 48 | "only-arrow-functions": false, 49 | "no-var-requires": false, 50 | "semicolon": false, 51 | "interface-over-type-literal": false, 52 | "align": false, 53 | "trailing-comma": false, 54 | "typedef": false, 55 | "newline-before-return": false, 56 | "interface-name": false, 57 | "ban-types": false, 58 | "no-relative-imports": false, 59 | "missing-jsdoc": false, 60 | "export-name": false, 61 | "no-require-imports": false, 62 | "no-http-string": false, 63 | "no-parameter-reassignment": false, 64 | "strict-boolean-expressions": false, 65 | "no-unsafe-any": false, 66 | "no-backbone-get-set-outside-model": false, 67 | "no-function-constructor-with-string-args": false, 68 | "no-reserved-keywords": false, 69 | "no-increment-decrement": false, 70 | "no-unnecessary-bind": false, 71 | "linebreak-style": false, 72 | "no-parameter-properties": false, 73 | "newline-per-chained-call": false 74 | } 75 | } 76 | --------------------------------------------------------------------------------