commit
22e36c41bc
104 changed files with 12608 additions and 0 deletions
-
7.editorconfig
-
76.github/CODE_OF_CONDUCT.md
-
49.github/CONTRIBUTING.md
-
243.gitignore
-
4.prettierrc
-
21LICENSE.md
-
69README.md
-
5_templates/generator/help/index.ejs
-
18_templates/generator/new/hello.ejs
-
18_templates/generator/with-prompt/hello.ejs
-
14_templates/generator/with-prompt/prompt.ejs
-
17_templates/module/empty-module/module.ts.ejs
-
10_templates/module/empty-module/prompt.js
-
6_templates/module/help/index.ejs
-
3_templates/module/with-crud/constants.ts.ejs
-
3_templates/module/with-crud/controller.spec.ts.ejs
-
96_templates/module/with-crud/controller.ts.ejs
-
15_templates/module/with-crud/dto.ts.ejs
-
16_templates/module/with-crud/entity.ts.ejs
-
24_templates/module/with-crud/module.ts.ejs
-
10_templates/module/with-crud/prompt.js
-
19_templates/module/with-crud/repository.ts.ejs
-
13_templates/module/with-crud/routing-body.ts.ejs
-
10_templates/module/with-crud/routing-head.ts.ejs
-
3_templates/module/with-crud/service.spec.ts.ejs
-
62_templates/module/with-crud/service.ts.ejs
-
18_templates/module/with-routing/module.ts.ejs
-
10_templates/module/with-routing/prompt.js
-
13_templates/module/with-routing/routing-body.ts.ejs
-
10_templates/module/with-routing/routing-head.ts.ejs
-
6compodoc.server.json
-
9docker/postgresql.yml
-
14exemple.env
-
5nest-cli.json
-
122package.json
-
21src/app.controller.spec.ts
-
13src/app.controller.ts
-
58src/app.module.ts
-
19src/app.routes.ts
-
8src/decorators/currentUser.decorator.ts
-
3src/decorators/public.decorator.ts
-
3src/decorators/roles.decorator.ts
-
0src/exceptions/.gitkeep
-
0src/filters/.gitkeep
-
24src/guards/roles.guard.ts
-
0src/interceptors/.gitkeep
-
0src/interfaces/.gitkeep
-
71src/main.ts
-
0src/middlewares/.gitkeep
-
40src/modules/core/auth/auth.controller.ts
-
17src/modules/core/auth/auth.dto.ts
-
29src/modules/core/auth/auth.guard.ts
-
33src/modules/core/auth/auth.module.ts
-
32src/modules/core/auth/auth.service.ts
-
22src/modules/core/auth/jwt.strategy.ts
-
9src/modules/core/config/config.module.ts
-
118src/modules/core/config/config.service.spec.ts
-
75src/modules/core/config/config.service.ts
-
8src/modules/core/crypto/crypto.module.ts
-
32src/modules/core/crypto/crypto.service.spec.ts
-
28src/modules/core/crypto/crypto.service.ts
-
43src/modules/core/logger/logger-exception.interceptor.ts
-
9src/modules/core/logger/logger.constants.ts
-
11src/modules/core/logger/logger.module.ts
-
33src/modules/core/logger/logger.providers.ts
-
21src/modules/core/logger/logger.service.spec.ts
-
46src/modules/core/logger/logger.service.ts
-
11src/modules/core/metrics/common/prom.decorators.ts
-
11src/modules/core/metrics/common/prom.utils.ts
-
66src/modules/core/metrics/interceptors/metrics.interceptor.ts
-
33src/modules/core/metrics/interfaces/metric.type.ts
-
27src/modules/core/metrics/interfaces/prom-options.interface.ts
-
90src/modules/core/metrics/metrics-core.module.ts
-
3src/modules/core/metrics/metrics.constants.ts
-
15src/modules/core/metrics/metrics.constroller.ts
-
147src/modules/core/metrics/metrics.module.ts
-
98src/modules/core/metrics/metrics.providers.ts
-
32src/modules/core/metrics/middleware/inbound.middleware.ts
-
10src/modules/core/pagination/pagination.decorator.ts
-
14src/modules/core/pagination/pagination.validation.ts
-
14src/modules/core/validation/validation.exception.ts
-
59src/modules/core/validation/validation.util.ts
-
24src/modules/core/validation/validator.pipe.ts
-
2src/modules/roles/roles.constants.ts
-
43src/modules/roles/roles.controller.ts
-
15src/modules/roles/roles.entity.ts
-
14src/modules/roles/roles.module.ts
-
14src/modules/roles/roles.repository.ts
-
41src/modules/roles/roles.service.ts
-
0src/modules/user/user.constants.ts
-
132src/modules/user/user.controller.ts
-
54src/modules/user/user.dto.ts
-
33src/modules/user/user.entity.ts
-
20src/modules/user/user.module.ts
-
22src/modules/user/user.repository.ts
-
112src/modules/user/user.service.ts
-
0src/pipes/.gitkeep
-
29src/utils/dbmodel.model.ts
-
28test/app.e2e-spec.ts
-
9test/jest-e2e.json
@ -0,0 +1,7 @@ |
|||
|
|||
# Unix-style newlines with a newline ending every file |
|||
[*] |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
indent_style = space |
|||
indent_size = 2 |
|||
@ -0,0 +1,76 @@ |
|||
# Contributor Covenant Code of Conduct |
|||
|
|||
## Our Pledge |
|||
|
|||
In the interest of fostering an open and welcoming environment, we as |
|||
contributors and maintainers pledge to making participation in our project and |
|||
our community a harassment-free experience for everyone, regardless of age, body |
|||
size, disability, ethnicity, sex characteristics, gender identity and expression, |
|||
level of experience, education, socio-economic status, nationality, personal |
|||
appearance, race, religion, or sexual identity and orientation. |
|||
|
|||
## Our Standards |
|||
|
|||
Examples of behavior that contributes to creating a positive environment |
|||
include: |
|||
|
|||
- Using welcoming and inclusive language |
|||
- Being respectful of differing viewpoints and experiences |
|||
- Gracefully accepting constructive criticism |
|||
- Focusing on what is best for the community |
|||
- Showing empathy towards other community members |
|||
|
|||
Examples of unacceptable behavior by participants include: |
|||
|
|||
- The use of sexualized language or imagery and unwelcome sexual attention or |
|||
advances |
|||
- Trolling, insulting/derogatory comments, and personal or political attacks |
|||
- Public or private harassment |
|||
- Publishing others' private information, such as a physical or electronic |
|||
address, without explicit permission |
|||
- Other conduct which could reasonably be considered inappropriate in a |
|||
professional setting |
|||
|
|||
## Our Responsibilities |
|||
|
|||
Project maintainers are responsible for clarifying the standards of acceptable |
|||
behavior and are expected to take appropriate and fair corrective action in |
|||
response to any instances of unacceptable behavior. |
|||
|
|||
Project maintainers have the right and responsibility to remove, edit, or |
|||
reject comments, commits, code, wiki edits, issues, and other contributions |
|||
that are not aligned to this Code of Conduct, or to ban temporarily or |
|||
permanently any contributor for other behaviors that they deem inappropriate, |
|||
threatening, offensive, or harmful. |
|||
|
|||
## Scope |
|||
|
|||
This Code of Conduct applies both within project spaces and in public spaces |
|||
when an individual is representing the project or its community. Examples of |
|||
representing a project or community include using an official project e-mail |
|||
address, posting via an official social media account, or acting as an appointed |
|||
representative at an online or offline event. Representation of a project may be |
|||
further defined and clarified by project maintainers. |
|||
|
|||
## Enforcement |
|||
|
|||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
|||
reported by contacting the project team at nic.beaussart@gmail.com. All |
|||
complaints will be reviewed and investigated and will result in a response that |
|||
is deemed necessary and appropriate to the circumstances. The project team is |
|||
obligated to maintain confidentiality with regard to the reporter of an incident. |
|||
Further details of specific enforcement policies may be posted separately. |
|||
|
|||
Project maintainers who do not follow or enforce the Code of Conduct in good |
|||
faith may face temporary or permanent repercussions as determined by other |
|||
members of the project's leadership. |
|||
|
|||
## Attribution |
|||
|
|||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
|||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
|||
|
|||
[homepage]: https://www.contributor-covenant.org |
|||
|
|||
For answers to common questions about this code of conduct, see |
|||
https://www.contributor-covenant.org/faq |
|||
@ -0,0 +1,49 @@ |
|||
# Contributing to this project |
|||
|
|||
First of all thank you for expressing interest in this project! :+1:<br /> |
|||
We are very happy to welcome new contributors. :tada: |
|||
|
|||
## How can I contribute? :man_technologist: |
|||
|
|||
### Report a bug :bug: |
|||
|
|||
If you think you have found a bug, please search [our issue tracker][issues] to see if anyone has already reported it.<br /> |
|||
If you are the first to have noticed it, please [create an issue][new_issue], and make sure to provide any information that might help us resolve it.<br /> |
|||
You are welcome to try and fix it by submitting a pull request if you would like to (see Pull requests section for more information). |
|||
|
|||
### Feature requests and enhancements :sparkles: |
|||
|
|||
We are open to feature requests, be sure to search [our issue tracker][issues] to see if anyone has already asked for it.<br /> |
|||
If not, please [create an issue][new_issue] describing what you want, what your use case is, and an example of code.<br /> |
|||
You are welcome to try and do it yourself by submitting a pull request if you would like to (see Pull requests section for more information).<br /> |
|||
As immutadot is still a very young project, we are also open to enhancement suggestions; if you think of anything that might help us improve the project, please [create an issue][new_issue] and we will be happy to discuss it with you. |
|||
|
|||
### Pull requests :arrow_up: |
|||
|
|||
#### Installation :package: |
|||
|
|||
We use [yarn](https://yarnpkg.com/) as our package manager.<br /> |
|||
We don't support [npm](https://www.npmjs.com/) due to the overhead of managing two lock files and the inconsistency of `package-lock.json` between Linux and MacOS (see npm/npm#17722).<br /> |
|||
Once you have cloned the project, run `yarn` to install all the dependencies. |
|||
|
|||
#### Tests and Code style :policeman: |
|||
|
|||
If you write any code, be sure to write the test that goes with it in a file located at the same place and named `<something>.spec.js`.<br /> |
|||
We use [tslint](https://palantir.github.io/tslint/) to enforce some coding rules (2 spaces indentation, etc.), ideally use an IDE to help you comply with these rules.<br /> |
|||
We use [prettier](https://prettier.io/) to have a uniform code style for everybody. It should run on pre commit automatically :tada:.<br /> |
|||
Before pushing anything, please be sure to check that the tests are OK by running `yarn test` and that your code complies with the coding rules by running `yarn lint && yarn format:check`. |
|||
|
|||
#### Documentation :bulb: |
|||
|
|||
The better the documentation, the fewer things users will have to wonder about.<br /> |
|||
We [use jsdoc](http://usejsdoc.org/) to document our code, if you write any new code please write the documentation with it and try to conform to the existing documention.<br /> |
|||
|
|||
#### emojis :beers: |
|||
|
|||
We really :heart: emojis, and we would like you to share our :heart:.<br /> |
|||
Each and every commit message has to be prefixed by an emoji.<br /> |
|||
Please refer to [the gitmoji guide](https://gitmoji.carloscuesta.me/) to know which one to use. |
|||
|
|||
## Any questions :question: |
|||
|
|||
If you are not sure whether to report a bug or ask for a new feature, or if you just have a question about anything, please [create an issue][new_issue]. |
|||
@ -0,0 +1,243 @@ |
|||
|
|||
# Created by https://www.gitignore.io/api/code,node,linux,windows,intellij,sublimetext |
|||
|
|||
### Code ### |
|||
# Visual Studio Code - https://code.visualstudio.com/ |
|||
.settings/ |
|||
.vscode/ |
|||
jsconfig.json |
|||
|
|||
### Intellij ### |
|||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm |
|||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 |
|||
|
|||
# User-specific stuff |
|||
.idea/**/workspace.xml |
|||
.idea/**/tasks.xml |
|||
.idea/**/usage.statistics.xml |
|||
.idea/**/dictionaries |
|||
.idea/**/shelf |
|||
|
|||
# Generated files |
|||
.idea/**/contentModel.xml |
|||
|
|||
# Sensitive or high-churn files |
|||
.idea/**/dataSources/ |
|||
.idea/**/dataSources.ids |
|||
.idea/**/dataSources.local.xml |
|||
.idea/**/sqlDataSources.xml |
|||
.idea/**/dynamic.xml |
|||
.idea/**/uiDesigner.xml |
|||
.idea/**/dbnavigator.xml |
|||
|
|||
# Gradle |
|||
.idea/**/gradle.xml |
|||
.idea/**/libraries |
|||
|
|||
# Gradle and Maven with auto-import |
|||
# When using Gradle or Maven with auto-import, you should exclude module files, |
|||
# since they will be recreated, and may cause churn. Uncomment if using |
|||
# auto-import. |
|||
# .idea/modules.xml |
|||
# .idea/*.iml |
|||
# .idea/modules |
|||
|
|||
# CMake |
|||
cmake-build-*/ |
|||
|
|||
# Mongo Explorer plugin |
|||
.idea/**/mongoSettings.xml |
|||
|
|||
# File-based project format |
|||
*.iws |
|||
|
|||
# IntelliJ |
|||
out/ |
|||
|
|||
# mpeltonen/sbt-idea plugin |
|||
.idea_modules/ |
|||
|
|||
# JIRA plugin |
|||
atlassian-ide-plugin.xml |
|||
|
|||
# Cursive Clojure plugin |
|||
.idea/replstate.xml |
|||
|
|||
# Crashlytics plugin (for Android Studio and IntelliJ) |
|||
com_crashlytics_export_strings.xml |
|||
crashlytics.properties |
|||
crashlytics-build.properties |
|||
fabric.properties |
|||
|
|||
# Editor-based Rest Client |
|||
.idea/httpRequests |
|||
|
|||
# Android studio 3.1+ serialized cache file |
|||
.idea/caches/build_file_checksums.ser |
|||
|
|||
### Intellij Patch ### |
|||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 |
|||
|
|||
# *.iml |
|||
# modules.xml |
|||
# .idea/misc.xml |
|||
# *.ipr |
|||
|
|||
# Sonarlint plugin |
|||
.idea/sonarlint |
|||
|
|||
.idea/ |
|||
|
|||
### Linux ### |
|||
*~ |
|||
|
|||
# temporary files which can be created if a process still has a handle open of a deleted file |
|||
.fuse_hidden* |
|||
|
|||
# KDE directory preferences |
|||
.directory |
|||
|
|||
# Linux trash folder which might appear on any partition or disk |
|||
.Trash-* |
|||
|
|||
# .nfs files are created when an open file is removed but is still being accessed |
|||
.nfs* |
|||
|
|||
### Node ### |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
*.pid.lock |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
|
|||
# nyc test coverage |
|||
.nyc_output |
|||
|
|||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# Bower dependency directory (https://bower.io/) |
|||
bower_components |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (https://nodejs.org/api/addons.html) |
|||
build/Release |
|||
|
|||
# Dependency directories |
|||
node_modules/ |
|||
jspm_packages/ |
|||
|
|||
# TypeScript v1 declaration files |
|||
typings/ |
|||
|
|||
# Optional npm cache directory |
|||
.npm |
|||
|
|||
# Optional eslint cache |
|||
.eslintcache |
|||
|
|||
# Optional REPL history |
|||
.node_repl_history |
|||
|
|||
# Output of 'npm pack' |
|||
*.tgz |
|||
|
|||
# Yarn Integrity file |
|||
.yarn-integrity |
|||
|
|||
# dotenv environment variables file |
|||
.env |
|||
|
|||
# parcel-bundler cache (https://parceljs.org/) |
|||
.cache |
|||
|
|||
# next.js build output |
|||
.next |
|||
|
|||
# nuxt.js build output |
|||
.nuxt |
|||
|
|||
# vuepress build output |
|||
.vuepress/dist |
|||
|
|||
# Serverless directories |
|||
.serverless |
|||
|
|||
### SublimeText ### |
|||
# Cache files for Sublime Text |
|||
*.tmlanguage.cache |
|||
*.tmPreferences.cache |
|||
*.stTheme.cache |
|||
|
|||
# Workspace files are user-specific |
|||
*.sublime-workspace |
|||
|
|||
# Project files should be checked into the repository, unless a significant |
|||
# proportion of contributors will probably not be using Sublime Text |
|||
# *.sublime-project |
|||
|
|||
# SFTP configuration file |
|||
sftp-config.json |
|||
|
|||
# Package control specific files |
|||
Package Control.last-run |
|||
Package Control.ca-list |
|||
Package Control.ca-bundle |
|||
Package Control.system-ca-bundle |
|||
Package Control.cache/ |
|||
Package Control.ca-certs/ |
|||
Package Control.merged-ca-bundle |
|||
Package Control.user-ca-bundle |
|||
oscrypto-ca-bundle.crt |
|||
bh_unicode_properties.cache |
|||
|
|||
# Sublime-github package stores a github token in this file |
|||
# https://packagecontrol.io/packages/sublime-github |
|||
GitHub.sublime-settings |
|||
|
|||
### Windows ### |
|||
# Windows thumbnail cache files |
|||
Thumbs.db |
|||
ehthumbs.db |
|||
ehthumbs_vista.db |
|||
|
|||
# Dump file |
|||
*.stackdump |
|||
|
|||
# Folder config file |
|||
[Dd]esktop.ini |
|||
|
|||
# Recycle Bin used on file shares |
|||
$RECYCLE.BIN/ |
|||
|
|||
# Windows Installer files |
|||
*.cab |
|||
*.msi |
|||
*.msix |
|||
*.msm |
|||
*.msp |
|||
|
|||
# Windows shortcuts |
|||
*.lnk |
|||
|
|||
|
|||
# End of https://www.gitignore.io/api/code,node,linux,windows,intellij,sublimetext |
|||
|
|||
.env |
|||
dist/ |
|||
doc-server |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"singleQuote": true, |
|||
"trailingComma": "all" |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2019 Nicolas Beaussart-Hatchuel |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -0,0 +1,69 @@ |
|||
<h1 align="center">Welcome to TP on CI and CircleCI 👋</h1> |
|||
|
|||
|
|||
You cand find the codelabs here on iCampus. |
|||
|
|||
## Installation |
|||
|
|||
```bash |
|||
$ yarn install |
|||
``` |
|||
|
|||
## Running the app |
|||
|
|||
```bash |
|||
# development |
|||
yarn start |
|||
|
|||
# watch mode |
|||
yarn start:dev |
|||
|
|||
# production mode |
|||
$ yarn start:prod |
|||
``` |
|||
|
|||
## Test |
|||
|
|||
```bash |
|||
# unit tests |
|||
$ yarn test |
|||
|
|||
# e2e tests |
|||
$ yarn test:e2e |
|||
|
|||
# test coverage |
|||
$ yarn test:cov |
|||
``` |
|||
|
|||
## Lints |
|||
|
|||
```bash |
|||
# Check |
|||
$ yarn format:check |
|||
|
|||
# Format code |
|||
$ yarn format:check |
|||
|
|||
$ yarn lint |
|||
``` |
|||
|
|||
## Generate |
|||
|
|||
```bash |
|||
$ yarn generate module with-crud --name myModule |
|||
``` |
|||
|
|||
## Author |
|||
|
|||
👤 **Nicolas Beaussart <nic.beaussart@gmail.com>** |
|||
|
|||
- Twitter: [@beaussan](https://twitter.com/beaussan) |
|||
- Github: [@beaussan](https://github.com/beaussan) |
|||
|
|||
## Show your support |
|||
|
|||
Give a ⭐️ if this project helped you! |
|||
|
|||
--- |
|||
|
|||
_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ |
|||
@ -0,0 +1,5 @@ |
|||
--- |
|||
message: | |
|||
hygen {bold generator new} --name [NAME] --action [ACTION] |
|||
hygen {bold generator with-prompt} --name [NAME] --action [ACTION] |
|||
--- |
|||
@ -0,0 +1,18 @@ |
|||
--- |
|||
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs |
|||
--- |
|||
--- |
|||
to: app/hello.js |
|||
--- |
|||
const hello = ``` |
|||
Hello! |
|||
This is your first hygen template. |
|||
|
|||
Learn what it can do here: |
|||
|
|||
https://github.com/jondot/hygen |
|||
``` |
|||
|
|||
console.log(hello) |
|||
|
|||
|
|||
@ -0,0 +1,18 @@ |
|||
--- |
|||
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs |
|||
--- |
|||
--- |
|||
to: app/hello.js |
|||
--- |
|||
const hello = ``` |
|||
Hello! |
|||
This is your first prompt based hygen template. |
|||
|
|||
Learn what it can do here: |
|||
|
|||
https://github.com/jondot/hygen |
|||
``` |
|||
|
|||
console.log(hello) |
|||
|
|||
|
|||
@ -0,0 +1,14 @@ |
|||
--- |
|||
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js |
|||
--- |
|||
|
|||
// see types of prompts: |
|||
// https://github.com/enquirer/enquirer/tree/master/examples |
|||
// |
|||
module.exports = [ |
|||
{ |
|||
type: 'input', |
|||
name: 'message', |
|||
message: "What's your message?" |
|||
} |
|||
] |
|||
@ -0,0 +1,17 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%>import { Module } from '@nestjs/common'; |
|||
|
|||
@Module({ |
|||
imports: [], |
|||
controllers: [], |
|||
providers: [], |
|||
exports: [], |
|||
}) |
|||
export class <%=properName%>Module {} |
|||
|
|||
|
|||
@ -0,0 +1,10 @@ |
|||
// see types of prompts:
|
|||
// https://github.com/enquirer/enquirer/tree/master/examples
|
|||
//
|
|||
module.exports = [ |
|||
{ |
|||
type: 'input', |
|||
name: 'name', |
|||
message: 'What is your module name?', |
|||
}, |
|||
]; |
|||
@ -0,0 +1,6 @@ |
|||
--- |
|||
message: | |
|||
hygen {bold module with-crud} --name [NAME] |
|||
hygen {bold module empty-module} --name [NAME] |
|||
hygen {bold module with-routing} --name [NAME] |
|||
--- |
|||
@ -0,0 +1,3 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.constants.ts |
|||
--- |
|||
@ -0,0 +1,3 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.controller.spec.ts |
|||
--- |
|||
@ -0,0 +1,96 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.controller.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
camelCase = h.changeCase.camel(name); |
|||
%>import { |
|||
Body, |
|||
Controller, |
|||
Delete, |
|||
Get, |
|||
NotFoundException, |
|||
Param, |
|||
ParseIntPipe, |
|||
Post, |
|||
Put, |
|||
Query, |
|||
} from '@nestjs/common'; |
|||
import { <%= properName %> } from './<%= kebabName %>.entity'; |
|||
import { <%= properName %>Service } from './<%= kebabName %>.service'; |
|||
import { |
|||
ApiBearerAuth, |
|||
ApiImplicitParam, |
|||
ApiResponse, |
|||
ApiUseTags, |
|||
} from '@nestjs/swagger'; |
|||
import { <%= properName %>Dto } from './<%= kebabName %>.dto'; |
|||
|
|||
|
|||
@ApiUseTags('{{ sentenceCase name }}') |
|||
@Controller() |
|||
// @ApiBearerAuth() |
|||
export class <%= properName %>Controller { |
|||
constructor(private readonly <%= camelCase %>Service: <%= properName %>Service) {} |
|||
|
|||
@Get() |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'Get a list of all {{ sentenceCase name }}.', |
|||
type: <%= properName %>, |
|||
isArray: true, |
|||
}) |
|||
getAll(): Promise<<%= properName %>[]> { |
|||
return this.<%= camelCase %>Service.getAll(); |
|||
} |
|||
|
|||
@Post() |
|||
@ApiResponse({ |
|||
status: 201, |
|||
description: 'The {{ sentenceCase name }} has been created.', |
|||
type: <%= properName %>, |
|||
}) |
|||
saveNew(@Body() <%= camelCase %>Dto: <%= properName %>Dto): Promise<<%= properName %>> { |
|||
return this.<%= camelCase %>Service.saveNew(<%= camelCase %>Dto); |
|||
} |
|||
|
|||
@Get(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The {{ sentenceCase name }} with the matching id', |
|||
type: <%= properName %>, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async findOne( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
): Promise<<%= properName %>> { |
|||
return (await this.<%= camelCase %>Service.getOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
} |
|||
|
|||
@Put(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The updated {{ sentenceCase name }} with the matching id', |
|||
type: <%= properName %>, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async updateOne( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
@Body() <%= camelCase %>Dto: <%= properName %>Dto, |
|||
): Promise<<%= properName %>> { |
|||
return this.<%= camelCase %>Service.update(id, <%= camelCase %>Dto); |
|||
} |
|||
|
|||
@Delete(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The {{ sentenceCase name }} with the matching id was deleted', |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async deleteOne(@Param('id', new ParseIntPipe()) id: number): Promise<void> { |
|||
await this.<%= camelCase %>Service.deleteById(id); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.dto.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
camelCase = h.changeCase.camel(name); |
|||
%>import { IsArray, IsOptional, IsString, Min, MinLength } from 'class-validator'; |
|||
import { Type } from 'class-transformer'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
|
|||
export class <%= properName %>Dto { |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,16 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.entity.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
camelCase = h.changeCase.camel(name); |
|||
%>import { DbAuditModel } from '../../utils/dbmodel.model'; |
|||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
|
|||
|
|||
@Entity() |
|||
export class <%= properName %> extends DbAuditModel { |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%>import { Module } from '@nestjs/common'; |
|||
import { TypeOrmModule } from '@nestjs/typeorm'; |
|||
import { <%=properName%>Controller } from './<%=kebabName%>.controller'; |
|||
import { <%=properName%>Service } from './<%=kebabName%>.service'; |
|||
import { <%=properName%> } from './<%=kebabName%>.entity'; |
|||
import { <%=properName%>Repository } from './<%=kebabName%>.repository'; |
|||
|
|||
@Module({ |
|||
imports: [ |
|||
TypeOrmModule.forFeature([<%=properName%>, <%=properName%>Repository]), |
|||
], |
|||
controllers: [<%=properName%>Controller], |
|||
providers: [<%=properName%>Service], |
|||
exports: [<%=properName%>Service], |
|||
}) |
|||
export class <%=properName%>Module {} |
|||
|
|||
|
|||
@ -0,0 +1,10 @@ |
|||
// see types of prompts:
|
|||
// https://github.com/enquirer/enquirer/tree/master/examples
|
|||
//
|
|||
module.exports = [ |
|||
{ |
|||
type: 'input', |
|||
name: 'name', |
|||
message: 'What is your module name?', |
|||
}, |
|||
]; |
|||
@ -0,0 +1,19 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.repository.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
camelCase = h.changeCase.camel(name); |
|||
%>import { EntityRepository, Repository } from 'typeorm'; |
|||
import { <%= properName %> } from './<%= kebabName %>.entity'; |
|||
import { Optional } from 'typescript-optional'; |
|||
|
|||
@EntityRepository(<%= properName %>) |
|||
export class <%= properName %>Repository extends Repository<<%= properName %>> { |
|||
async findOneById(id: number): Promise<Optional<<%= properName %>>> { |
|||
return Optional.ofNullable( |
|||
await this.findOne(id, {}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
--- |
|||
to: src/app.routes.ts |
|||
after: export const appRoutes.* |
|||
inject: true |
|||
skip_if: /<%= h.changeCase.param(name) %> |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%> { |
|||
path: '/<%= kebabName %>s', |
|||
module: <%= properName %>Module, |
|||
}, |
|||
@ -0,0 +1,10 @@ |
|||
--- |
|||
to: src/app.routes.ts |
|||
after: import { Routes } from 'nest-router'; |
|||
inject: true |
|||
skip_if: modules/<%= h.changeCase.param(name) %> |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%>import { <%= properName %>Module } from './modules/<%= kebabName %>/<%= kebabName %>.module'; |
|||
@ -0,0 +1,3 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.service.spec.ts |
|||
--- |
|||
@ -0,0 +1,62 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.service.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
camelCase = h.changeCase.camel(name); |
|||
%>import { <%= properName %> } from './<%= kebabName %>.entity'; |
|||
import { Inject, Injectable, NotFoundException } from '@nestjs/common'; |
|||
import { InjectRepository } from '@nestjs/typeorm'; |
|||
import { <%= properName %>Repository } from './<%= kebabName %>.repository'; |
|||
// import { } from './<%= kebabName %>.constants'; |
|||
import { <%= properName %>Dto } from './<%= kebabName %>.dto'; |
|||
import { Optional } from 'typescript-optional'; |
|||
|
|||
|
|||
@Injectable() |
|||
export class <%= properName %>Service { |
|||
|
|||
constructor( |
|||
@InjectRepository(<%= properName %>Repository) |
|||
private readonly <%= camelCase %>Repository: <%= properName %>Repository, |
|||
) { } |
|||
|
|||
async getAll(): Promise<<%= properName %>[]> { |
|||
return this.<%= camelCase %>Repository.find({}); |
|||
} |
|||
|
|||
async getOneById(id: number): Promise<Optional<<%= properName %>>> { |
|||
return this.<%= camelCase %>Repository.findOneById(id); |
|||
} |
|||
|
|||
async saveNew(body: <%= properName %>Dto): Promise<<%= properName %>> { |
|||
let <%= camelCase %>New = new <%= properName %>(); |
|||
|
|||
// Complete with the mappings |
|||
|
|||
<%= camelCase %>New = await this.<%= camelCase %>Repository.save(<%= camelCase %>New); |
|||
|
|||
return <%= camelCase %>New; |
|||
} |
|||
|
|||
async update(id: number, body: <%= properName %>Dto): Promise<<%= properName %>> { |
|||
let <%= camelCase %>Found = (await this.<%= camelCase %>Repository.findOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
|
|||
// Complete with the mappings |
|||
|
|||
<%= camelCase %>Found = await this.<%= camelCase %>Repository.save(<%= camelCase %>Found); |
|||
|
|||
return <%= camelCase %>Found; |
|||
} |
|||
|
|||
async deleteById(id: number): Promise<void> { |
|||
const <%= camelCase %>Found = (await this.<%= camelCase %>Repository.findOneById( |
|||
id, |
|||
)).orElseThrow(() => new NotFoundException()); |
|||
await this.<%= camelCase %>Repository.remove(<%= camelCase %>Found); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,18 @@ |
|||
--- |
|||
to: src/modules/<%=h.changeCase.param(name)%>/<%=h.changeCase.param(name)%>.module.ts |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%>import { Module } from '@nestjs/common'; |
|||
import { TypeOrmModule } from '@nestjs/typeorm'; |
|||
|
|||
@Module({ |
|||
imports: [], |
|||
controllers: [], |
|||
providers: [], |
|||
exports: [], |
|||
}) |
|||
export class <%=properName%>Module {} |
|||
|
|||
|
|||
@ -0,0 +1,10 @@ |
|||
// see types of prompts:
|
|||
// https://github.com/enquirer/enquirer/tree/master/examples
|
|||
//
|
|||
module.exports = [ |
|||
{ |
|||
type: 'input', |
|||
name: 'name', |
|||
message: 'What is your module name?', |
|||
}, |
|||
]; |
|||
@ -0,0 +1,13 @@ |
|||
--- |
|||
to: src/app.routes.ts |
|||
after: export const appRoutes.* |
|||
inject: true |
|||
skip_if: /<%= h.changeCase.param(name) %> |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%> { |
|||
path: '/<%= kebabName %>s', |
|||
module: <%= properName %>Module, |
|||
}, |
|||
@ -0,0 +1,10 @@ |
|||
--- |
|||
to: src/app.routes.ts |
|||
after: import { Routes } from 'nest-router'; |
|||
inject: true |
|||
skip_if: modules/<%= h.changeCase.param(name) %> |
|||
--- |
|||
<% |
|||
properName = h.changeCase.pascal(name); |
|||
kebabName = h.changeCase.param(name); |
|||
%>import { <%= properName %>Module } from './modules/<%= kebabName %>/<%= kebabName %>.module'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"port": 9911, |
|||
"name": "TP_CI SERVER", |
|||
"output": "doc-server", |
|||
"tsconfig": "./tsconfig.json" |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
version: '2' |
|||
services: |
|||
ognion-boilerplate-postgresql: |
|||
image: postgres:9.6.5 |
|||
environment: |
|||
- POSTGRES_USER=tpCi |
|||
- POSTGRES_PASSWORD=someNotSecurePassword |
|||
ports: |
|||
- 5432:5432 |
|||
@ -0,0 +1,14 @@ |
|||
## API |
|||
API_PORT=3000 |
|||
API_HOST=localhost |
|||
API_PROTOCOL=http |
|||
|
|||
## LOGGER |
|||
LOG_LEVEL=debug |
|||
LOG_SQL_REQUEST=true |
|||
|
|||
## DB [TypeORM] |
|||
DATABASE_URL=postgres://tpCi:someNotSecurePassword@localhost:5432/tpCi |
|||
|
|||
## AUTHENTICATION [JWT] |
|||
JWT_SECRET=bananana |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"language": "ts", |
|||
"collection": "@nestjs/schematics", |
|||
"sourceRoot": "src" |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
{ |
|||
"name": "ci-tp", |
|||
"version": "0.0.1", |
|||
"description": "A CI tp for M1 III", |
|||
"author": "Nicolas Beaussart <nic.beaussart@gmail.com>", |
|||
"license": "MIT", |
|||
"scripts": { |
|||
"prebuild": "rimraf dist", |
|||
"build": "nest build", |
|||
"start": "nest start", |
|||
"start:dev": "nest start --watch", |
|||
"start:debug": "nest start --debug --watch", |
|||
"start:prod": "node dist/main", |
|||
"lint": "tslint -p tsconfig.json -c tslint.json", |
|||
"test": "jest", |
|||
"test:ci": "jest --runInBand --coverage", |
|||
"doc:build": "./node_modules/.bin/compodoc -c compodoc.server.json", |
|||
"doc": "./node_modules/.bin/compodoc -c compodoc.server.json -s -o -w", |
|||
"test:watch": "jest --watch", |
|||
"test:cov": "jest --coverage", |
|||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", |
|||
"test:e2e": "jest --config ./test/jest-e2e.json", |
|||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"./**/*.{yaml,yml}\"", |
|||
"format:check": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\" \"./**/*.{yaml,yml}\"", |
|||
"generate": "hygen" |
|||
}, |
|||
"dependencies": { |
|||
"@hapi/joi": "^16.1.5", |
|||
"@nestjs/common": "^6.1.1", |
|||
"@nestjs/core": "^6.1.1", |
|||
"@nestjs/jwt": "^6.0.0", |
|||
"@nestjs/passport": "^6.0.0", |
|||
"@nestjs/platform-express": "^6.1.1", |
|||
"@nestjs/swagger": "^3.0.2", |
|||
"@nestjs/typeorm": "^6.1.0", |
|||
"argon2": "0.25.1", |
|||
"class-sanitizer": "^0.0.5", |
|||
"class-transformer": "^0.2.2", |
|||
"class-validator": "^0.10.1", |
|||
"dotenv": "^8.0.0", |
|||
"nest-router": "^1.0.9", |
|||
"passport": "^0.4.0", |
|||
"passport-jwt": "^4.0.0", |
|||
"pg": "^7.10.0", |
|||
"prom-client": "^11.3.0", |
|||
"reflect-metadata": "^0.1.13", |
|||
"rxjs": "^6.5.2", |
|||
"swagger-ui-express": "^4.0.2", |
|||
"typeorm": "^0.2.17", |
|||
"typescript": "^3.4.5", |
|||
"typescript-optional": "^2.0.1", |
|||
"url-value-parser": "^2.0.1", |
|||
"winston": "^3.2.1" |
|||
}, |
|||
"devDependencies": { |
|||
"@compodoc/compodoc": "^1.1.11", |
|||
"@nestjs/cli": "^6.10.1", |
|||
"@nestjs/testing": "^6.8.0", |
|||
"@types/express": "^4.16.1", |
|||
"@types/jest": "^24.0.12", |
|||
"@types/joi": "^14.3.3", |
|||
"@types/node": "^12.0.0", |
|||
"@types/supertest": "^2.0.5", |
|||
"husky": "^3.0.8", |
|||
"hygen": "^5.0.1", |
|||
"jest": "^24.8.0", |
|||
"lint-staged": "^9.4.1", |
|||
"prettier": "^1.17.0", |
|||
"rimraf": "^3.0.0", |
|||
"supertest": "^4.0.2", |
|||
"ts-jest": "^24.0.2", |
|||
"ts-loader": "^6.0.0", |
|||
"ts-node": "^8.1.0", |
|||
"tsconfig-paths": "^3.8.0", |
|||
"tslint": "5.20.0", |
|||
"tslint-config-prettier": "^1.18.0" |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"pre-commit": "lint-staged" |
|||
} |
|||
}, |
|||
"lint-staged": { |
|||
"*.ts": [ |
|||
"prettier --write", |
|||
"tslint -p tsconfig.json -c tslint.json", |
|||
"git add" |
|||
], |
|||
"*.{js,json,md}": [ |
|||
"prettier --write", |
|||
"git add" |
|||
] |
|||
}, |
|||
"jest": { |
|||
"moduleFileExtensions": [ |
|||
"js", |
|||
"json", |
|||
"ts" |
|||
], |
|||
"rootDir": "src", |
|||
"testRegex": ".spec.ts$", |
|||
"transform": { |
|||
"^.+\\.(t|j)s$": "ts-jest" |
|||
}, |
|||
"coverageDirectory": "../coverage", |
|||
"testEnvironment": "node" |
|||
}, |
|||
"greenkeeper": { |
|||
"commitMessages": { |
|||
"initialBadge": ":pencil: Add Greenkeeper badge", |
|||
"initialDependencies": ":arrow_up: Upgrade: Update dependencies", |
|||
"initialBranches": ":tada: Build: Whitelist greenkeeper branches", |
|||
"dependencyUpdate": ":arrow_up: Upgrade: Update ${dependency} to version ${version}", |
|||
"devDependencyUpdate": ":arrow_up: Upgrade: Update ${dependency} to version ${version}", |
|||
"dependencyPin": ":pushpin: Pin ${dependency} to ${oldVersion}", |
|||
"devDependencyPin": ":pushpin: Pin ${dependency} to ${oldVersion}", |
|||
"addConfigFile": ":wrench: Add Greenkeeper config file", |
|||
"updateConfigFile": ":wrench: Update Greenkeeper config file", |
|||
"lockfileUpdate": ":wrench: Update lockfile ${lockfilePath}" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { INestApplication } from '@nestjs/common'; |
|||
import { AppController } from './app.controller'; |
|||
|
|||
describe('AppController', () => { |
|||
let app: TestingModule; |
|||
|
|||
beforeAll(async () => { |
|||
app = await Test.createTestingModule({ |
|||
controllers: [AppController], |
|||
providers: [], |
|||
}).compile(); |
|||
}); |
|||
|
|||
describe('ping', () => { |
|||
it('should return "Hello World!"', () => { |
|||
const appController = app.get<AppController>(AppController); |
|||
expect(appController.root()).toBe('pong'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,13 @@ |
|||
import { Get, Controller } from '@nestjs/common'; |
|||
import { Public } from './decorators/public.decorator'; |
|||
|
|||
@Controller() |
|||
export class AppController { |
|||
constructor() {} |
|||
|
|||
@Get('ping') |
|||
@Public() |
|||
root(): string { |
|||
return 'pong'; |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
import { |
|||
ClassSerializerInterceptor, |
|||
MiddlewareConsumer, |
|||
Module, |
|||
NestModule, |
|||
} from '@nestjs/common'; |
|||
import { AppController } from './app.controller'; |
|||
import { LoggerModule } from './modules/core/logger/logger.module'; |
|||
import { RouterModule } from 'nest-router'; |
|||
import { appRoutes } from './app.routes'; |
|||
import { ConfigModule } from './modules/core/config/config.module'; |
|||
import { RolesGuard } from './guards/roles.guard'; |
|||
import { UserModule } from './modules/user/user.module'; |
|||
import { ConfigService } from './modules/core/config/config.service'; |
|||
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; |
|||
|
|||
import { RolesModule } from './modules/roles/roles.module'; |
|||
import { AuthModule } from './modules/core/auth/auth.module'; |
|||
import { PromModule } from './modules/core/metrics/metrics.module'; |
|||
import { InboundMiddleware } from './modules/core/metrics/middleware/inbound.middleware'; |
|||
// needle-module-import
|
|||
|
|||
@Module({ |
|||
imports: [ |
|||
ConfigModule, // Global
|
|||
TypeOrmModule.forRootAsync({ |
|||
imports: [ConfigModule], |
|||
useFactory: async ( |
|||
configService: ConfigService, |
|||
): Promise<TypeOrmModuleOptions> => ({ |
|||
type: 'postgres', |
|||
url: configService.databaseUrl, |
|||
entities: [__dirname + '/**/*.entity{.ts,.js}'], |
|||
synchronize: true, |
|||
logging: configService.isLoggingDb ? 'all' : false, |
|||
}), |
|||
inject: [ConfigService], |
|||
}), |
|||
PromModule.forRoot({ |
|||
defaultLabels: { |
|||
app: 'v1.0.0', |
|||
}, |
|||
}), |
|||
LoggerModule, // Global
|
|||
RouterModule.forRoutes(appRoutes), |
|||
AuthModule, |
|||
UserModule, |
|||
RolesModule, |
|||
// needle-module-includes
|
|||
], |
|||
controllers: [AppController], |
|||
providers: [RolesGuard, ClassSerializerInterceptor], |
|||
}) |
|||
export class AppModule implements NestModule { |
|||
configure(consumer: MiddlewareConsumer): MiddlewareConsumer | void { |
|||
consumer.apply(InboundMiddleware).forRoutes('*'); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import { Routes } from 'nest-router'; |
|||
import { UserModule } from './modules/user/user.module'; |
|||
import { RolesModule } from './modules/roles/roles.module'; |
|||
import { AuthController } from './modules/core/auth/auth.controller'; |
|||
|
|||
export const appRoutes: Routes = [ |
|||
{ |
|||
path: '/users', |
|||
module: UserModule, |
|||
}, |
|||
{ |
|||
path: '/roles', |
|||
module: RolesModule, |
|||
}, |
|||
{ |
|||
path: '/auth', |
|||
module: AuthController, |
|||
}, |
|||
]; |
|||
@ -0,0 +1,8 @@ |
|||
import { createParamDecorator } from '@nestjs/common'; |
|||
import { User } from '../modules/user/user.entity'; |
|||
|
|||
export const CurrentUser = createParamDecorator( |
|||
(data: any, req: any): User => { |
|||
return req.user; |
|||
}, |
|||
); |
|||
@ -0,0 +1,3 @@ |
|||
import { SetMetadata } from '@nestjs/common'; |
|||
|
|||
export const Public = () => SetMetadata('isPublic', true); |
|||
@ -0,0 +1,3 @@ |
|||
import { SetMetadata } from '@nestjs/common'; |
|||
|
|||
export const Roles = (...roles: string[]) => SetMetadata('roles', roles); |
|||
@ -0,0 +1,24 @@ |
|||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; |
|||
import { Reflector } from '@nestjs/core'; |
|||
import { User } from '../modules/user/user.entity'; |
|||
import { Role } from '../modules/roles/roles.entity'; |
|||
|
|||
@Injectable() |
|||
export class RolesGuard implements CanActivate { |
|||
constructor(private readonly reflector: Reflector) {} |
|||
|
|||
canActivate(context: ExecutionContext): boolean { |
|||
const roles = this.reflector.get<string[]>('roles', context.getHandler()); |
|||
|
|||
if (!roles) { |
|||
return true; |
|||
} |
|||
|
|||
const request = context.switchToHttp().getRequest(); |
|||
const user: User = request.user; |
|||
const hasRole = () => |
|||
user.roles.some((role: Role) => roles.includes(role.name)); |
|||
|
|||
return user && user.roles && hasRole(); |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
import { NestFactory, Reflector } from '@nestjs/core'; |
|||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; |
|||
import { config } from 'dotenv'; |
|||
import { AppModule } from './app.module'; |
|||
import { LoggerExceptionInterceptor } from './modules/core/logger/logger-exception.interceptor'; |
|||
import { LoggerModule } from './modules/core/logger/logger.module'; |
|||
import { ValidatorPipe } from './modules/core/validation/validator.pipe'; |
|||
import { LoggerService } from './modules/core/logger/logger.service'; |
|||
import { RolesGuard } from './guards/roles.guard'; |
|||
import { ConfigService } from './modules/core/config/config.service'; |
|||
import { ConfigModule } from './modules/core/config/config.module'; |
|||
import { NestExpressApplication } from '@nestjs/platform-express'; |
|||
import { ClassSerializerInterceptor } from '@nestjs/common'; |
|||
import { InboundMiddleware } from './modules/core/metrics/middleware/inbound.middleware'; |
|||
import { PromModule } from './modules/core/metrics/metrics.module'; |
|||
import { MetricsInterceptor } from './modules/core/metrics/interceptors/metrics.interceptor'; |
|||
import { AuthGuard } from './modules/core/auth/auth.guard'; |
|||
|
|||
async function bootstrap() { |
|||
// Use .env to configure environment variables (process.env)
|
|||
config(); |
|||
|
|||
const app = await NestFactory.create<NestExpressApplication>(AppModule); |
|||
|
|||
// Set logger
|
|||
app.useLogger(app.get(LoggerService)); |
|||
|
|||
// Enable cors
|
|||
app.enableCors(); |
|||
|
|||
// Interceptors
|
|||
const loggerInterceptor = app |
|||
.select(LoggerModule) |
|||
.get(LoggerExceptionInterceptor); |
|||
const classSerializer = app.select(AppModule).get(ClassSerializerInterceptor); |
|||
const metricsInterceptor = app.select(AppModule).get(MetricsInterceptor); |
|||
app.useGlobalInterceptors( |
|||
metricsInterceptor, |
|||
loggerInterceptor, // Log exceptions
|
|||
classSerializer, |
|||
); |
|||
|
|||
const connfigService = app.select(ConfigModule).get(ConfigService); |
|||
|
|||
// Guards
|
|||
const reflector = app.get(Reflector); |
|||
const rolesGuard = app.select(AppModule).get(RolesGuard); |
|||
app.useGlobalGuards(new AuthGuard(reflector), rolesGuard); |
|||
|
|||
// Validators
|
|||
app.useGlobalPipes( |
|||
new ValidatorPipe(), // Validate inputs
|
|||
); |
|||
|
|||
// Swagger
|
|||
const options = new DocumentBuilder() |
|||
.setTitle('Boilerplate nest') |
|||
.setDescription('The boilerplate API description') |
|||
.setVersion('0.0.1') |
|||
.addBearerAuth() |
|||
.build(); |
|||
const document = SwaggerModule.createDocument(app, options); |
|||
SwaggerModule.setup('/docs', app, document); |
|||
|
|||
const server = await app.listen(connfigService.port); |
|||
app |
|||
.get(LoggerService) |
|||
.info(`Application is listening on port ${connfigService.port}.`); |
|||
return server; |
|||
} |
|||
bootstrap(); |
|||
@ -0,0 +1,40 @@ |
|||
import { ApiUseTags, ApiBearerAuth } from '@nestjs/swagger'; |
|||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; |
|||
import { LoginDto, TokenDto } from './auth.dto'; |
|||
import { AuthService } from './auth.service'; |
|||
import { User } from '../../user/user.entity'; |
|||
import { UserService } from '../../user/user.service'; |
|||
import { UserDtoRegister } from '../../user/user.dto'; |
|||
import { CurrentUser } from '../../../decorators/currentUser.decorator'; |
|||
import { Public } from '../../../decorators/public.decorator'; |
|||
|
|||
@ApiUseTags('Auth') |
|||
@Controller() |
|||
export class AuthController { |
|||
constructor( |
|||
private readonly authService: AuthService, |
|||
private readonly userService: UserService, |
|||
) {} |
|||
|
|||
@Public() |
|||
@Post('login') |
|||
async signIn(@Body() userLogin: LoginDto): Promise<TokenDto> { |
|||
const token = await this.authService.signIn( |
|||
userLogin.email, |
|||
userLogin.password, |
|||
); |
|||
return { token }; |
|||
} |
|||
|
|||
@Public() |
|||
@Post('register') |
|||
async registerUser(@Body() userRegister: UserDtoRegister): Promise<User> { |
|||
return this.userService.saveNew(userRegister); |
|||
} |
|||
|
|||
@Get('me') |
|||
@ApiBearerAuth() |
|||
async getMe(@CurrentUser() loggedUser: User) { |
|||
return loggedUser; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
import { IsEmail, MinLength } from 'class-validator'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
|
|||
export class TokenDto { |
|||
token: string; |
|||
} |
|||
|
|||
export class LoginDto { |
|||
@MinLength(1) |
|||
@IsEmail() |
|||
@ApiModelProperty({ example: 'foo@bar.fr' }) |
|||
email: string; |
|||
|
|||
@MinLength(1) |
|||
@ApiModelProperty({ example: 'azerty' }) |
|||
password: string; |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import { |
|||
ExecutionContext, |
|||
Injectable, |
|||
UnauthorizedException, |
|||
} from '@nestjs/common'; |
|||
import { AuthGuard as NestAuthGuard } from '@nestjs/passport'; |
|||
import { Reflector } from '@nestjs/core'; |
|||
|
|||
@Injectable() |
|||
export class AuthGuard extends NestAuthGuard('jwt') { |
|||
public constructor(private readonly reflector: Reflector) { |
|||
super(); |
|||
} |
|||
|
|||
canActivate(context: ExecutionContext) { |
|||
const isPublic = this.reflector.get<boolean>( |
|||
'isPublic', |
|||
context.getHandler(), |
|||
); |
|||
|
|||
if (isPublic) { |
|||
return true; |
|||
} |
|||
|
|||
// Add your custom authentication logic here
|
|||
// for example, call super.logIn(request) to establish a session.
|
|||
return super.canActivate(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
import { Global, Module } from '@nestjs/common'; |
|||
import { AuthService } from './auth.service'; |
|||
import { UserModule } from '../../user/user.module'; |
|||
import { PassportModule } from '@nestjs/passport'; |
|||
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; |
|||
import { ConfigModule } from '../config/config.module'; |
|||
import { ConfigService } from '../config/config.service'; |
|||
import { JwtStrategy } from './jwt.strategy'; |
|||
import { AuthController } from './auth.controller'; |
|||
|
|||
@Global() |
|||
@Module({ |
|||
imports: [ |
|||
UserModule, |
|||
// ConfigModule,
|
|||
PassportModule.register({ defaultStrategy: 'jwt' }), |
|||
JwtModule.registerAsync({ |
|||
imports: [ConfigModule], |
|||
inject: [ConfigService], |
|||
useFactory: async ( |
|||
configService: ConfigService, |
|||
): Promise<JwtModuleOptions> => ({ |
|||
secretOrPrivateKey: configService.jwtSecret, |
|||
signOptions: { |
|||
expiresIn: 3600, |
|||
}, |
|||
}), |
|||
}), |
|||
], |
|||
providers: [AuthService, JwtStrategy], |
|||
controllers: [AuthController], |
|||
}) |
|||
export class AuthModule {} |
|||
@ -0,0 +1,32 @@ |
|||
import { Injectable, UnauthorizedException } from '@nestjs/common'; |
|||
import { UserService } from '../../user/user.service'; |
|||
import { JwtService } from '@nestjs/jwt'; |
|||
|
|||
export interface JwtPayload { |
|||
idUser: number; |
|||
} |
|||
|
|||
@Injectable() |
|||
export class AuthService { |
|||
constructor( |
|||
private readonly usersService: UserService, |
|||
private readonly jwtService: JwtService, |
|||
) {} |
|||
|
|||
async signIn(email: string, password: string): Promise<string> { |
|||
const userFound = (await this.usersService.getOnWithEmail( |
|||
email, |
|||
)).orElseThrow(() => new UnauthorizedException()); |
|||
if (!(await this.usersService.doPasswordMatch(userFound, password))) { |
|||
throw new UnauthorizedException(); |
|||
} |
|||
const user: JwtPayload = { idUser: userFound.id }; |
|||
return this.jwtService.sign(user); |
|||
} |
|||
|
|||
async validateUser(payload: JwtPayload): Promise<any> { |
|||
return (await this.usersService.getOneById(payload.idUser)).orElseThrow( |
|||
() => new UnauthorizedException(), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { AuthService, JwtPayload } from './auth.service'; |
|||
import { PassportStrategy } from '@nestjs/passport'; |
|||
import { ExtractJwt, Strategy } from 'passport-jwt'; |
|||
import { ConfigService } from '../config/config.service'; |
|||
|
|||
@Injectable() |
|||
export class JwtStrategy extends PassportStrategy(Strategy) { |
|||
constructor( |
|||
private readonly authService: AuthService, |
|||
private readonly configService: ConfigService, |
|||
) { |
|||
super({ |
|||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), |
|||
secretOrKey: configService.jwtSecret, |
|||
}); |
|||
} |
|||
|
|||
async validate(payload: JwtPayload) { |
|||
return await this.authService.validateUser(payload); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { Global, Module } from '@nestjs/common'; |
|||
import { ConfigService } from './config.service'; |
|||
|
|||
@Global() |
|||
@Module({ |
|||
providers: [ConfigService], |
|||
exports: [ConfigService], |
|||
}) |
|||
export class ConfigModule {} |
|||
@ -0,0 +1,118 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { ConfigService } from './config.service'; |
|||
import { expand } from 'rxjs/operators'; |
|||
|
|||
describe('ConfigService', () => { |
|||
const OLD_ENV = process.env; |
|||
|
|||
beforeEach(() => { |
|||
jest.resetModules(); // this is important - it clears the cache
|
|||
process.env = { ...OLD_ENV }; |
|||
delete process.env.NODE_ENV; |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
process.env = OLD_ENV; |
|||
}); |
|||
|
|||
const loadService = async (): Promise<ConfigService> => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [ConfigService], |
|||
}).compile(); |
|||
|
|||
return module.get<ConfigService>(ConfigService); |
|||
}; |
|||
|
|||
it('should have default values', async () => { |
|||
const dbUrl = 'gotodb please'; |
|||
const jwtSecrets = 'shhhh, secret'; |
|||
|
|||
// set the variables
|
|||
process.env.NODE_ENV = undefined; |
|||
process.env.API_PORT = undefined; |
|||
process.env.API_PROTOCOL = undefined; |
|||
process.env.LOG_LEVEL = undefined; |
|||
process.env.LOG_SQL_REQUEST = undefined; |
|||
process.env.DATABASE_URL = dbUrl; |
|||
process.env.JWT_SECRET = jwtSecrets; |
|||
|
|||
const configService = await loadService(); |
|||
|
|||
expect(configService.nodeEnv).toEqual('development'); |
|||
expect(configService.port).toEqual(3000); |
|||
expect(configService.loggerLevel).toEqual('debug'); |
|||
expect(configService.databaseUrl).toEqual(dbUrl); |
|||
expect(configService.jwtSecret).toEqual(jwtSecrets); |
|||
expect(configService.isLoggingDb).toEqual(false); |
|||
}); |
|||
|
|||
it('should error when no db url set', async () => { |
|||
const jwtSecrets = 'shhhh, secret'; |
|||
|
|||
// set the variables
|
|||
process.env.NODE_ENV = undefined; |
|||
process.env.API_PORT = undefined; |
|||
process.env.API_PROTOCOL = undefined; |
|||
process.env.LOG_LEVEL = undefined; |
|||
process.env.LOG_SQL_REQUEST = undefined; |
|||
process.env.DATABASE_URL = undefined; |
|||
process.env.JWT_SECRET = jwtSecrets; |
|||
|
|||
try { |
|||
const configService = await loadService(); |
|||
fail(); |
|||
} catch (e) { |
|||
expect(e.message).toEqual( |
|||
'Config validation error: "DATABASE_URL" is required', |
|||
); |
|||
} |
|||
}); |
|||
|
|||
it('should error when no jwt token url set', async () => { |
|||
const dbUrl = 'gotodb please'; |
|||
|
|||
// set the variables
|
|||
process.env.NODE_ENV = undefined; |
|||
process.env.API_PORT = undefined; |
|||
process.env.API_PROTOCOL = undefined; |
|||
process.env.LOG_LEVEL = undefined; |
|||
process.env.LOG_SQL_REQUEST = undefined; |
|||
process.env.DATABASE_URL = dbUrl; |
|||
process.env.JWT_SECRET = undefined; |
|||
|
|||
try { |
|||
const configService = await loadService(); |
|||
fail(); |
|||
} catch (e) { |
|||
expect(e.message).toEqual( |
|||
'Config validation error: "JWT_SECRET" is required', |
|||
); |
|||
} |
|||
}); |
|||
|
|||
it('should set correct values', async () => { |
|||
const dbUrl = 'gotodb please'; |
|||
const jwtSecrets = 'shhhh, secret'; |
|||
const nodeEnv = 'test'; |
|||
const apiPort = 2020; |
|||
const logLevel = 'silly'; |
|||
const logSqlRequest = true; |
|||
|
|||
// set the variables
|
|||
process.env.NODE_ENV = nodeEnv; |
|||
process.env.API_PORT = `${apiPort}`; |
|||
process.env.LOG_LEVEL = logLevel; |
|||
process.env.LOG_SQL_REQUEST = `${logSqlRequest}`; |
|||
process.env.DATABASE_URL = dbUrl; |
|||
process.env.JWT_SECRET = jwtSecrets; |
|||
|
|||
const configService = await loadService(); |
|||
|
|||
expect(configService.nodeEnv).toEqual(nodeEnv); |
|||
expect(configService.port).toEqual(apiPort); |
|||
expect(configService.loggerLevel).toEqual(logLevel); |
|||
expect(configService.databaseUrl).toEqual(dbUrl); |
|||
expect(configService.jwtSecret).toEqual(jwtSecrets); |
|||
expect(configService.isLoggingDb).toEqual(logSqlRequest); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,75 @@ |
|||
import { Injectable } from '@nestjs/common'; |
|||
import * as Joi from '@hapi/joi'; |
|||
import { config as parseConfig } from 'dotenv'; |
|||
|
|||
interface EnvConfig { |
|||
[key: string]: any; |
|||
} |
|||
|
|||
@Injectable() |
|||
export class ConfigService { |
|||
private readonly envConfig: EnvConfig; |
|||
|
|||
constructor() { |
|||
try { |
|||
parseConfig(); |
|||
} catch (e) {} |
|||
|
|||
this.envConfig = this.validateInput(process.env); |
|||
} |
|||
|
|||
/** |
|||
* Ensures all needed variables are set, and returns the validated JavaScript object |
|||
* including the applied default values. |
|||
*/ |
|||
private validateInput(envConfig: EnvConfig): EnvConfig { |
|||
const envVarsSchema: Joi.ObjectSchema = Joi.object({ |
|||
NODE_ENV: Joi.string() |
|||
.valid('development', 'production', 'test', 'provision') |
|||
.default('development'), |
|||
API_PORT: Joi.number().default(3000), |
|||
LOG_LEVEL: Joi.string() |
|||
.valid('error', 'warning', 'info', 'debug', 'silly') |
|||
.default('debug'), |
|||
DATABASE_URL: Joi.string().required(), |
|||
JWT_SECRET: Joi.string().required(), |
|||
LOG_SQL_REQUEST: Joi.boolean().default(false), |
|||
}); |
|||
|
|||
const { error, value: validatedEnvConfig } = envVarsSchema.validate( |
|||
envConfig, |
|||
{ |
|||
stripUnknown: true, |
|||
}, |
|||
); |
|||
|
|||
if (error) { |
|||
throw new Error(`Config validation error: ${error.message}`); |
|||
} |
|||
return validatedEnvConfig; |
|||
} |
|||
|
|||
get nodeEnv(): string { |
|||
return this.envConfig.NODE_ENV; |
|||
} |
|||
|
|||
get databaseUrl(): string { |
|||
return this.envConfig.DATABASE_URL; |
|||
} |
|||
|
|||
get jwtSecret(): string { |
|||
return this.envConfig.JWT_SECRET; |
|||
} |
|||
|
|||
get isLoggingDb(): boolean { |
|||
return this.envConfig.LOG_SQL_REQUEST; |
|||
} |
|||
|
|||
get loggerLevel(): string { |
|||
return this.envConfig.LOG_LEVEL; |
|||
} |
|||
|
|||
get port(): number { |
|||
return this.envConfig.API_PORT; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { CryptoService } from './crypto.service'; |
|||
|
|||
@Module({ |
|||
providers: [CryptoService], |
|||
exports: [CryptoService], |
|||
}) |
|||
export class CryptoModule {} |
|||
@ -0,0 +1,32 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { CryptoService } from './crypto.service'; |
|||
import * as argon from 'argon2'; |
|||
|
|||
describe('CryptoService', () => { |
|||
let service: CryptoService; |
|||
beforeAll(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [CryptoService], |
|||
}).compile(); |
|||
service = module.get<CryptoService>(CryptoService); |
|||
}); |
|||
it('should be defined', () => { |
|||
expect(service).toBeDefined(); |
|||
}); |
|||
|
|||
it('should hash a password and verify if it is correct', async () => { |
|||
const toHash = 'some random string'; |
|||
|
|||
const hashed = await service.hash(toHash); |
|||
|
|||
expect(await service.compare(toHash, hashed)).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should hash a password and return false if not same password', async () => { |
|||
const toHash = 'some random string'; |
|||
|
|||
const hashed = await service.hash(toHash); |
|||
|
|||
expect(await service.compare(toHash + 'A', hashed)).toBeFalsy(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,28 @@ |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { argon2id, hash, verify } from 'argon2'; |
|||
|
|||
@Injectable() |
|||
export class CryptoService { |
|||
private readonly type = argon2id; |
|||
|
|||
constructor() {} |
|||
|
|||
/** |
|||
* Compare hash |
|||
* @param {string} plain |
|||
* @param {string} hash |
|||
* @returns {Promise<boolean>} |
|||
*/ |
|||
public async compare(plain: string, hashString: string): Promise<boolean> { |
|||
return await verify(hashString, plain); |
|||
} |
|||
|
|||
/** |
|||
* Generate hash |
|||
* @param {string} plain |
|||
* @returns {Promise<string>} |
|||
*/ |
|||
public async hash(plain: string): Promise<string> { |
|||
return await hash(plain, { type: this.type }); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
import { |
|||
CallHandler, |
|||
ExecutionContext, |
|||
HttpException, |
|||
Injectable, |
|||
NestInterceptor, |
|||
} from '@nestjs/common'; |
|||
import { Observable } from 'rxjs'; |
|||
import { catchError } from 'rxjs/operators'; |
|||
import { LoggerService } from './logger.service'; |
|||
|
|||
@Injectable() |
|||
export class LoggerExceptionInterceptor implements NestInterceptor { |
|||
constructor(private loggerService: LoggerService) {} |
|||
|
|||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { |
|||
const request = context.switchToHttp().getRequest(); |
|||
return next.handle().pipe( |
|||
catchError(exception => { |
|||
if (exception instanceof HttpException) { |
|||
// If 500, log as error
|
|||
if (500 <= exception.getStatus()) |
|||
this.loggerService.error( |
|||
'HttpException ' + exception.getStatus(), |
|||
request.path, |
|||
exception.getResponse(), |
|||
); |
|||
// Else log as debug (we don't want 4xx errors in production)
|
|||
else { |
|||
this.loggerService.debug( |
|||
'HttpException ' + exception.getStatus(), |
|||
request.path, |
|||
exception.getResponse(), |
|||
); |
|||
} |
|||
} else { |
|||
this.loggerService.error('Unexpected error', request.path, exception); |
|||
} |
|||
throw exception; |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
export const LOGGER_WINSTON_PROVIDER = 'LOGGER_WINSTON_PROVIDER'; |
|||
|
|||
export enum LOGGER_LEVEL { |
|||
ERROR = 'error', |
|||
WARNING = 'warning', |
|||
INFO = 'info', |
|||
DEBUG = 'debug', |
|||
SILLY = 'silly', |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import { Global, Module } from '@nestjs/common'; |
|||
import { LoggerService } from './logger.service'; |
|||
import { LoggerExceptionInterceptor } from './logger-exception.interceptor'; |
|||
import { loggerProviders } from './logger.providers'; |
|||
|
|||
@Global() |
|||
@Module({ |
|||
providers: [...loggerProviders, LoggerService, LoggerExceptionInterceptor], |
|||
exports: [LoggerService, LoggerExceptionInterceptor], |
|||
}) |
|||
export class LoggerModule {} |
|||
@ -0,0 +1,33 @@ |
|||
import { LOGGER_LEVEL, LOGGER_WINSTON_PROVIDER } from './logger.constants'; |
|||
import { createLogger, transports, format } from 'winston'; |
|||
|
|||
export const loggerProviders = [ |
|||
{ |
|||
provide: LOGGER_WINSTON_PROVIDER, |
|||
useFactory: () => { |
|||
const LOG_LEVEL = LOGGER_LEVEL.DEBUG; |
|||
|
|||
const winstonTransports = [new transports.Console({})]; |
|||
|
|||
const winstonFormaters = format.combine( |
|||
format.colorize(), |
|||
format.timestamp({ |
|||
format: 'YYYY-MM-DD HH:mm:ss', |
|||
}), |
|||
format.align(), |
|||
format.printf( |
|||
info => |
|||
`[Nest] ${process.pid} - ${info.timestamp} ${ |
|||
info.level |
|||
}: ${info.message.trim()}`,
|
|||
), |
|||
); |
|||
|
|||
return createLogger({ |
|||
level: LOG_LEVEL, |
|||
transports: winstonTransports, |
|||
format: winstonFormaters, |
|||
}); |
|||
}, |
|||
}, |
|||
]; |
|||
@ -0,0 +1,21 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { LoggerService } from './logger.service'; |
|||
import { ConfigService } from '../config/config.service'; |
|||
import { loggerProviders } from './logger.providers'; |
|||
|
|||
describe('LoggerService', () => { |
|||
let service: LoggerService; |
|||
beforeAll(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [ |
|||
LoggerService, |
|||
{ provide: ConfigService, useValue: { loggerLevel: 'info' } }, |
|||
...loggerProviders, |
|||
], |
|||
}).compile(); |
|||
service = module.get<LoggerService>(LoggerService); |
|||
}); |
|||
it('should be defined', () => { |
|||
expect(service).toBeDefined(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,46 @@ |
|||
import { |
|||
Inject, |
|||
Injectable, |
|||
LoggerService as NestLoggerService, |
|||
} from '@nestjs/common'; |
|||
import { LOGGER_LEVEL, LOGGER_WINSTON_PROVIDER } from './logger.constants'; |
|||
import { Logger } from 'winston'; |
|||
import { ConfigService } from '../config/config.service'; |
|||
|
|||
@Injectable() |
|||
export class LoggerService implements NestLoggerService { |
|||
constructor( |
|||
@Inject(LOGGER_WINSTON_PROVIDER) private readonly winston: Logger, |
|||
private readonly configService: ConfigService, |
|||
) { |
|||
winston.transports[0].level = this.configService.loggerLevel; |
|||
} |
|||
|
|||
private logMessage(level: LOGGER_LEVEL, msg: string, ...meta): void { |
|||
this.winston.log(level, msg, ...meta); |
|||
} |
|||
|
|||
public log(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.INFO, msg, ...meta); |
|||
} |
|||
|
|||
public debug(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.DEBUG, msg, ...meta); |
|||
} |
|||
|
|||
public error(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.ERROR, msg, ...meta); |
|||
} |
|||
|
|||
public warn(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.WARNING, msg, ...meta); |
|||
} |
|||
|
|||
public info(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.INFO, msg, ...meta); |
|||
} |
|||
|
|||
public silly(msg: string, ...meta): void { |
|||
this.logMessage(LOGGER_LEVEL.SILLY, msg, ...meta); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import { Inject } from '@nestjs/common'; |
|||
import { getMetricToken } from './prom.utils'; |
|||
|
|||
export const InjectCounterMetric = (name: string) => |
|||
Inject(getMetricToken(`Counter`, name)); |
|||
export const InjectGaugeMetric = (name: string) => |
|||
Inject(getMetricToken(`Gauge`, name)); |
|||
export const InjectHistogramMetric = (name: string) => |
|||
Inject(getMetricToken(`Histogram`, name)); |
|||
export const InjectSummaryMetric = (name: string) => |
|||
Inject(getMetricToken(`Summary`, name)); |
|||
@ -0,0 +1,11 @@ |
|||
export function getMetricToken(type: string, name: string) { |
|||
return `${name}${type}`; |
|||
} |
|||
|
|||
export function getRegistryName(name: string) { |
|||
return `${name}PromRegistry`; |
|||
} |
|||
|
|||
export function getOptionsName(name: string) { |
|||
return `${name}PromOptions`; |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
import { |
|||
Injectable, |
|||
NestInterceptor, |
|||
ExecutionContext, |
|||
CallHandler, |
|||
} from '@nestjs/common'; |
|||
import { Observable, throwError } from 'rxjs'; |
|||
import { catchError, tap } from 'rxjs/operators'; |
|||
import { Counter, Histogram } from 'prom-client'; |
|||
import { InjectHistogramMetric } from '../common/prom.decorators'; |
|||
import * as UrlValueParser from 'url-value-parser'; |
|||
import { parse } from 'url'; |
|||
|
|||
@Injectable() |
|||
export class MetricsInterceptor implements NestInterceptor<any, any> { |
|||
parser: any; |
|||
|
|||
constructor( |
|||
@InjectHistogramMetric('http_request_duration_seconds') |
|||
private readonly httpRequests: Histogram, |
|||
) {} |
|||
|
|||
intercept( |
|||
context: ExecutionContext, |
|||
next: CallHandler<any>, |
|||
): Observable<any> | Promise<Observable<any>> { |
|||
const req = context.switchToHttp().getRequest(); |
|||
const path = req.originalUrl || req.url; |
|||
|
|||
if (path.match(/\/metrics(\?.*?)?$/)) { |
|||
return next.handle(); |
|||
} |
|||
|
|||
const labels: any = {}; |
|||
const timer = this.httpRequests.startTimer({ |
|||
path: this.getPath(req), |
|||
method: req.method, |
|||
}); |
|||
|
|||
return next.handle().pipe( |
|||
tap(() => { |
|||
const res = context.switchToHttp().getResponse(); |
|||
timer({ status_code: this.getStatusCode(res) }); |
|||
}), |
|||
catchError(err => { |
|||
const statusCode = err.status ? err.status : '500'; |
|||
timer({ status_code: statusCode }); |
|||
return throwError(err); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
getStatusCode(res: any): number { |
|||
return res.status_code || res.statusCode; |
|||
} |
|||
|
|||
getPath(req: any): string { |
|||
const path = parse(req.originalUrl || req.url).pathname; |
|||
|
|||
if (!this.parser) { |
|||
this.parser = new UrlValueParser(); |
|||
} |
|||
|
|||
return this.parser.replacePathValues(path); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
import * as PromClient from 'prom-client'; |
|||
|
|||
export enum MetricType { |
|||
Counter, |
|||
Gauge, |
|||
Histogram, |
|||
Summary, |
|||
} |
|||
|
|||
export interface MetricTypeConfigurationInterface { |
|||
type: MetricType; |
|||
configuration?: any; |
|||
} |
|||
|
|||
export class MetricTypeCounter implements MetricTypeConfigurationInterface { |
|||
type: MetricType = MetricType.Counter; |
|||
configuration: PromClient.CounterConfiguration; |
|||
} |
|||
|
|||
export class MetricTypeGauge implements MetricTypeConfigurationInterface { |
|||
type: MetricType = MetricType.Gauge; |
|||
configuration: PromClient.GaugeConfiguration; |
|||
} |
|||
|
|||
export class MetricTypeHistogram implements MetricTypeConfigurationInterface { |
|||
type: MetricType = MetricType.Histogram; |
|||
configuration: PromClient.HistogramConfiguration; |
|||
} |
|||
|
|||
export class MetricTypeSummary implements MetricTypeConfigurationInterface { |
|||
type: MetricType = MetricType.Summary; |
|||
configuration: PromClient.SummaryConfiguration; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
export interface PromModuleOptions { |
|||
[key: string]: any; |
|||
|
|||
/** |
|||
* Enable default metrics |
|||
* Under the hood, that call collectDefaultMetrics() |
|||
*/ |
|||
withDefaultsMetrics?: boolean; |
|||
|
|||
/** |
|||
* Enable internal controller to expose /metrics |
|||
* Caution: If you have a global prefix, don't forget to prefix it in prom |
|||
*/ |
|||
withDefaultController?: boolean; |
|||
|
|||
/** |
|||
* Create automatically http_requests_total counter |
|||
*/ |
|||
useHttpCounterMiddleware?: boolean; |
|||
|
|||
registryName?: string; |
|||
timeout?: number; |
|||
prefix?: string; |
|||
defaultLabels?: { |
|||
[key: string]: any; |
|||
}; |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
import { Global, DynamicModule, Module } from '@nestjs/common'; |
|||
import { ModuleRef } from '@nestjs/core'; |
|||
import { PromModuleOptions } from './interfaces/prom-options.interface'; |
|||
import { |
|||
DEFAULT_PROM_REGISTRY, |
|||
PROM_REGISTRY_NAME, |
|||
DEFAULT_PROM_OPTIONS, |
|||
} from './metrics.constants'; |
|||
|
|||
import * as client from 'prom-client'; |
|||
import { |
|||
Registry, |
|||
collectDefaultMetrics, |
|||
DefaultMetricsCollectorConfiguration, |
|||
} from 'prom-client'; |
|||
import { getRegistryName, getOptionsName } from './common/prom.utils'; |
|||
|
|||
@Global() |
|||
@Module({}) |
|||
export class PromCoreModule { |
|||
constructor(private readonly moduleRef: ModuleRef) {} |
|||
|
|||
static forRoot(options: PromModuleOptions = {}): DynamicModule { |
|||
const { |
|||
withDefaultsMetrics, |
|||
registryName, |
|||
timeout, |
|||
prefix, |
|||
...promOptions |
|||
} = options; |
|||
|
|||
const promRegistryName = registryName |
|||
? getRegistryName(registryName) |
|||
: DEFAULT_PROM_REGISTRY; |
|||
|
|||
const promRegistryNameProvider = { |
|||
provide: PROM_REGISTRY_NAME, |
|||
useValue: promRegistryName, |
|||
}; |
|||
|
|||
// const promOptionName = registryName ?
|
|||
// getOptionsName(registryName)
|
|||
// : DEFAULT_PROM_OPTIONS;
|
|||
|
|||
const promRegistryOptionsProvider = { |
|||
provide: DEFAULT_PROM_OPTIONS, |
|||
useValue: options, |
|||
}; |
|||
|
|||
const registryProvider = { |
|||
provide: promRegistryName, |
|||
useFactory: (): Registry => { |
|||
let registry = client.register; |
|||
if (promRegistryName !== DEFAULT_PROM_REGISTRY) { |
|||
registry = new Registry(); |
|||
} |
|||
|
|||
if (withDefaultsMetrics !== false) { |
|||
const defaultMetricsOptions: DefaultMetricsCollectorConfiguration = { |
|||
register: registry, |
|||
}; |
|||
if (timeout) { |
|||
defaultMetricsOptions.timeout = timeout; |
|||
} |
|||
if (prefix) { |
|||
defaultMetricsOptions.prefix = prefix; |
|||
} |
|||
collectDefaultMetrics(defaultMetricsOptions); |
|||
} |
|||
|
|||
return registry; |
|||
}, |
|||
}; |
|||
|
|||
return { |
|||
module: PromCoreModule, |
|||
providers: [ |
|||
promRegistryNameProvider, |
|||
promRegistryOptionsProvider, |
|||
registryProvider, |
|||
], |
|||
exports: [registryProvider], |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* on destroy |
|||
*/ |
|||
onModuleDestroy() {} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export const DEFAULT_PROM_REGISTRY = 'PromRegistry'; |
|||
export const DEFAULT_PROM_OPTIONS = 'PromOptions'; |
|||
export const PROM_REGISTRY_NAME = 'PromRegistryName'; |
|||
@ -0,0 +1,15 @@ |
|||
import { Controller, Get, Res } from '@nestjs/common'; |
|||
import * as client from 'prom-client'; |
|||
import { ApiExcludeEndpoint } from '@nestjs/swagger'; |
|||
import { Public } from '../../../decorators/public.decorator'; |
|||
|
|||
@Controller('metrics') |
|||
export class PromController { |
|||
@Public() |
|||
@ApiExcludeEndpoint() |
|||
@Get() |
|||
index(@Res() res) { |
|||
res.set('Content-Type', client.register.contentType); |
|||
res.end(client.register.metrics()); |
|||
} |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
import { |
|||
Module, |
|||
DynamicModule, |
|||
NestModule, |
|||
MiddlewareConsumer, |
|||
Inject, |
|||
RequestMethod, |
|||
} from '@nestjs/common'; |
|||
import { PromCoreModule } from './metrics-core.module'; |
|||
import { |
|||
createPromCounterProvider, |
|||
createPromGaugeProvider, |
|||
createPromHistogramProvider, |
|||
createPromSummaryProvider, |
|||
} from './metrics.providers'; |
|||
import * as client from 'prom-client'; |
|||
import { InboundMiddleware } from './middleware/inbound.middleware'; |
|||
import { DEFAULT_PROM_OPTIONS } from './metrics.constants'; |
|||
import { PromModuleOptions } from './interfaces/prom-options.interface'; |
|||
import { |
|||
MetricType, |
|||
MetricTypeConfigurationInterface, |
|||
} from './interfaces/metric.type'; |
|||
import { PromController } from './metrics.constroller'; |
|||
import { MetricsInterceptor } from './interceptors/metrics.interceptor'; |
|||
import { APP_INTERCEPTOR } from '@nestjs/core'; |
|||
|
|||
@Module({}) |
|||
export class PromModule { |
|||
static forRoot(options: PromModuleOptions = {}): DynamicModule { |
|||
const { withDefaultController, ...promOptions } = options; |
|||
|
|||
// @ts-ignore
|
|||
const moduleForRoot: Required<DynamicModule> = { |
|||
module: PromModule, |
|||
imports: [PromCoreModule.forRoot(options)], |
|||
controllers: [PromController], |
|||
exports: [], |
|||
providers: [InboundMiddleware, MetricsInterceptor], |
|||
}; |
|||
|
|||
// if want to use the http counter
|
|||
/* |
|||
if (useHttpCounterMiddleware) { |
|||
const inboundProvider = createPromCounterProvider({ |
|||
name: 'http_requests_total', |
|||
help: 'http_requests_total Number of inbound request', |
|||
labelNames: ['method'], |
|||
}); |
|||
|
|||
moduleForRoot.providers = [...moduleForRoot.providers, inboundProvider]; |
|||
moduleForRoot.exports = [...moduleForRoot.exports, inboundProvider]; |
|||
} |
|||
*/ |
|||
|
|||
const requestProvider = createPromHistogramProvider({ |
|||
name: 'http_request_duration_seconds', |
|||
help: |
|||
'duration histogram of http responses labeled with: method, status_code and path', |
|||
labelNames: ['method', 'status_code', 'path'], |
|||
buckets: [0.003, 0.03, 0.1, 0.3, 1.5, 10], |
|||
}); |
|||
|
|||
const inboundProvider = createPromCounterProvider({ |
|||
name: 'http_requests_total', |
|||
help: 'http_requests_total Number of inbound request', |
|||
labelNames: ['method'], |
|||
}); |
|||
|
|||
moduleForRoot.providers = [ |
|||
...moduleForRoot.providers, |
|||
requestProvider, |
|||
inboundProvider, |
|||
]; |
|||
|
|||
moduleForRoot.exports = [ |
|||
...moduleForRoot.exports, |
|||
requestProvider, |
|||
inboundProvider, |
|||
]; |
|||
|
|||
return moduleForRoot; |
|||
} |
|||
|
|||
static forMetrics( |
|||
metrics: MetricTypeConfigurationInterface[], |
|||
): DynamicModule { |
|||
const providers = metrics.map(entry => { |
|||
switch (entry.type) { |
|||
case MetricType.Counter: |
|||
return createPromCounterProvider(entry.configuration); |
|||
case MetricType.Gauge: |
|||
return createPromGaugeProvider(entry.configuration); |
|||
case MetricType.Histogram: |
|||
return createPromHistogramProvider(entry.configuration); |
|||
case MetricType.Summary: |
|||
return createPromSummaryProvider(entry.configuration); |
|||
default: |
|||
throw new ReferenceError(`The type ${entry.type} is not supported`); |
|||
} |
|||
}); |
|||
|
|||
return { |
|||
providers, |
|||
module: PromModule, |
|||
exports: providers, |
|||
}; |
|||
} |
|||
|
|||
static forCounter(configuration: client.CounterConfiguration): DynamicModule { |
|||
const provider = createPromCounterProvider(configuration); |
|||
return { |
|||
module: PromModule, |
|||
providers: [provider], |
|||
exports: [provider], |
|||
}; |
|||
} |
|||
|
|||
static forGauge(configuration: client.GaugeConfiguration): DynamicModule { |
|||
const provider = createPromGaugeProvider(configuration); |
|||
return { |
|||
module: PromModule, |
|||
providers: [provider], |
|||
exports: [provider], |
|||
}; |
|||
} |
|||
|
|||
static forHistogram( |
|||
configuration: client.HistogramConfiguration, |
|||
): DynamicModule { |
|||
const provider = createPromHistogramProvider(configuration); |
|||
return { |
|||
module: PromModule, |
|||
providers: [provider], |
|||
exports: [provider], |
|||
}; |
|||
} |
|||
|
|||
static forSummary(configuration: client.SummaryConfiguration): DynamicModule { |
|||
const provider = createPromSummaryProvider(configuration); |
|||
return { |
|||
module: PromModule, |
|||
providers: [provider], |
|||
exports: [provider], |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
import { DEFAULT_PROM_REGISTRY } from './metrics.constants'; |
|||
import { |
|||
Counter, |
|||
CounterConfiguration, |
|||
GaugeConfiguration, |
|||
Gauge, |
|||
HistogramConfiguration, |
|||
Histogram, |
|||
SummaryConfiguration, |
|||
Summary, |
|||
Registry, |
|||
} from 'prom-client'; |
|||
import { Provider } from '@nestjs/common'; |
|||
import { getMetricToken, getRegistryName } from './common/prom.utils'; |
|||
|
|||
export function createPromCounterProvider( |
|||
configuration: CounterConfiguration, |
|||
registryName: string = DEFAULT_PROM_REGISTRY, |
|||
): Provider { |
|||
return { |
|||
provide: getMetricToken('Counter', configuration.name), |
|||
useFactory: (registry: Registry) => { |
|||
const obj = new Counter({ |
|||
...configuration, |
|||
registers: [registry], |
|||
}); |
|||
return obj; |
|||
}, |
|||
inject: [ |
|||
registryName === DEFAULT_PROM_REGISTRY |
|||
? DEFAULT_PROM_REGISTRY |
|||
: getRegistryName(registryName), |
|||
], |
|||
}; |
|||
} |
|||
|
|||
export function createPromGaugeProvider( |
|||
configuration: GaugeConfiguration, |
|||
registryName: string = DEFAULT_PROM_REGISTRY, |
|||
): Provider { |
|||
return { |
|||
provide: getMetricToken('Gauge', configuration.name), |
|||
useFactory: (registry: Registry) => { |
|||
const obj = new Gauge({ |
|||
...configuration, |
|||
registers: [registry], |
|||
}); |
|||
return obj; |
|||
}, |
|||
inject: [ |
|||
registryName === DEFAULT_PROM_REGISTRY |
|||
? DEFAULT_PROM_REGISTRY |
|||
: getRegistryName(registryName), |
|||
], |
|||
}; |
|||
} |
|||
|
|||
export function createPromHistogramProvider( |
|||
configuration: HistogramConfiguration, |
|||
registryName: string = DEFAULT_PROM_REGISTRY, |
|||
): Provider { |
|||
return { |
|||
provide: getMetricToken('Histogram', configuration.name), |
|||
useFactory: (registry: Registry) => { |
|||
const obj = new Histogram({ |
|||
...configuration, |
|||
registers: [registry], |
|||
}); |
|||
return obj; |
|||
}, |
|||
inject: [ |
|||
registryName === DEFAULT_PROM_REGISTRY |
|||
? DEFAULT_PROM_REGISTRY |
|||
: getRegistryName(registryName), |
|||
], |
|||
}; |
|||
} |
|||
|
|||
export function createPromSummaryProvider( |
|||
configuration: SummaryConfiguration, |
|||
registryName: string = DEFAULT_PROM_REGISTRY, |
|||
): Provider { |
|||
return { |
|||
provide: getMetricToken('Summary', configuration.name), |
|||
useFactory: (registry: Registry) => { |
|||
const obj = new Summary({ |
|||
...configuration, |
|||
registers: [registry], |
|||
}); |
|||
return obj; |
|||
}, |
|||
inject: [ |
|||
registryName === DEFAULT_PROM_REGISTRY |
|||
? DEFAULT_PROM_REGISTRY |
|||
: getRegistryName(registryName), |
|||
], |
|||
}; |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
import { Injectable, NestMiddleware } from '@nestjs/common'; |
|||
import { Counter } from 'prom-client'; |
|||
import { Request, Response } from 'express'; |
|||
import { InjectCounterMetric } from '../common/prom.decorators'; |
|||
|
|||
@Injectable() |
|||
export class InboundMiddleware implements NestMiddleware { |
|||
constructor( |
|||
@InjectCounterMetric('http_requests_total') |
|||
private readonly _counter: Counter, |
|||
) {} |
|||
|
|||
use(req: Request, res: Response, next: () => void) { |
|||
const url = req.baseUrl; |
|||
const method = req.method; |
|||
|
|||
// ignore favicon
|
|||
if (url.endsWith('/favicon.ico')) { |
|||
next(); |
|||
return; |
|||
} |
|||
|
|||
// ignore metrics itself
|
|||
if (url.match(/\/metrics(\?.*?)?$/)) { |
|||
next(); |
|||
return; |
|||
} |
|||
|
|||
this._counter.labels(method).inc(1, new Date()); |
|||
next(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import { createParamDecorator } from '@nestjs/common'; |
|||
import { Request } from 'express'; |
|||
import { VPagination } from './pagination.validation'; |
|||
import { validateSync } from '../validation/validation.util'; |
|||
|
|||
export const Pagination = createParamDecorator( |
|||
(data: any, req: Request): VPagination => { |
|||
return validateSync(VPagination, req.query); |
|||
}, |
|||
); |
|||
@ -0,0 +1,14 @@ |
|||
import { IsNumber, Min } from 'class-validator'; |
|||
import { Type } from 'class-transformer'; |
|||
|
|||
export class VPagination { |
|||
@IsNumber() |
|||
@Min(0) |
|||
@Type(() => Number) |
|||
page: number = 0; |
|||
|
|||
@IsNumber() |
|||
@Min(1) |
|||
@Type(() => Number) |
|||
limit: number = 10; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { HttpException, HttpStatus } from '@nestjs/common'; |
|||
|
|||
export class ValidationException extends HttpException { |
|||
constructor(errors: string | object | any) { |
|||
super( |
|||
HttpException.createBody( |
|||
errors, |
|||
'VALIDATION_EXCEPTION', |
|||
HttpStatus.BAD_REQUEST, |
|||
), |
|||
HttpStatus.BAD_REQUEST, |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
import { plainToClass } from 'class-transformer'; |
|||
import { sanitize } from 'class-sanitizer'; |
|||
import { |
|||
validate as classValidate, |
|||
validateSync as classValidateSync, |
|||
} from 'class-validator'; |
|||
import { ValidationException } from './validation.exception'; |
|||
import { ClassType } from 'class-transformer/ClassTransformer'; |
|||
|
|||
/** |
|||
* Validate value for validator |
|||
* @param {ClassType<T>} validation |
|||
* @param {object} value |
|||
* @return {Promise<T>} |
|||
*/ |
|||
export const validate = async <T>( |
|||
validation: ClassType<T>, |
|||
value: object, |
|||
): Promise<T> => { |
|||
// Transform to class
|
|||
const entity = plainToClass<T, object>(validation, value); |
|||
|
|||
// Sanitize
|
|||
sanitize(entity); |
|||
|
|||
// Validate
|
|||
const errors = await classValidate(entity, { |
|||
skipMissingProperties: true, |
|||
whitelist: true, |
|||
}); |
|||
if (errors.length > 0) { |
|||
throw new ValidationException(errors); |
|||
} |
|||
|
|||
return entity; |
|||
}; |
|||
|
|||
/** |
|||
* Validate value for validator without async validators |
|||
* @param {ClassType<T>} validation |
|||
* @param {object} value |
|||
* @return {T} |
|||
*/ |
|||
export const validateSync = <T>(validation: ClassType<T>, value: object): T => { |
|||
// Transform to class
|
|||
const entity = plainToClass<T, object>(validation, value); |
|||
|
|||
// Sanitize
|
|||
sanitize(entity); |
|||
|
|||
// Validate
|
|||
const errors = classValidateSync(entity, { |
|||
skipMissingProperties: true, |
|||
whitelist: true, |
|||
}); |
|||
if (errors.length > 0) throw new ValidationException(errors); |
|||
|
|||
return entity; |
|||
}; |
|||
@ -0,0 +1,24 @@ |
|||
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; |
|||
import { isNil } from '@nestjs/common/utils/shared.utils'; |
|||
import { validate } from './validation.util'; |
|||
|
|||
@Injectable() |
|||
export class ValidatorPipe implements PipeTransform<any> { |
|||
public async transform(value: any, metadata: ArgumentMetadata): Promise<any> { |
|||
const { metatype } = metadata; |
|||
if (!metatype || !this.toValidate(metadata)) { |
|||
return value; |
|||
} |
|||
|
|||
return await validate(metatype, value); |
|||
} |
|||
|
|||
private toValidate(metadata: ArgumentMetadata): boolean { |
|||
const { metatype, type } = metadata; |
|||
if (type === 'custom') { |
|||
return false; |
|||
} |
|||
const types = [String, Boolean, Number, Array, Object]; |
|||
return !types.find(typeIn => metatype === typeIn) && !isNil(metatype); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export const USER_ROLE = 'USER_ROLE'; |
|||
export const ADMIN_ROLE = 'ADMIN_ROLE'; |
|||
@ -0,0 +1,43 @@ |
|||
import { |
|||
Controller, |
|||
Get, |
|||
NotFoundException, |
|||
Param, |
|||
ParseIntPipe, |
|||
UseGuards, |
|||
} from '@nestjs/common'; |
|||
import { Role } from './roles.entity'; |
|||
import { RolesService } from './roles.service'; |
|||
import { ApiBearerAuth, ApiResponse, ApiUseTags } from '@nestjs/swagger'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
|
|||
@ApiUseTags('Role') |
|||
@Controller() |
|||
@ApiBearerAuth() |
|||
export class RolesController { |
|||
constructor(private readonly rolesService: RolesService) {} |
|||
|
|||
@Get() |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'Get a list of all Role.', |
|||
type: Role, |
|||
isArray: true, |
|||
}) |
|||
getAll(): Promise<Role[]> { |
|||
return this.rolesService.getAll(); |
|||
} |
|||
|
|||
@Get(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The Role with the matching id', |
|||
type: Role, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async findOne(@Param('id', new ParseIntPipe()) id: number): Promise<Role> { |
|||
return (await this.rolesService.getOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
import { DbAuditModel } from '../../utils/dbmodel.model'; |
|||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
|
|||
@Entity() |
|||
export class Role extends DbAuditModel { |
|||
constructor(name: string) { |
|||
super(); |
|||
this.name = name; |
|||
} |
|||
|
|||
@ApiModelProperty({ required: true, readOnly: true }) |
|||
@Column({ unique: true }) |
|||
name: string; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { TypeOrmModule } from '@nestjs/typeorm'; |
|||
import { RolesController } from './roles.controller'; |
|||
import { RolesService } from './roles.service'; |
|||
import { Role } from './roles.entity'; |
|||
import { RolesRepository } from './roles.repository'; |
|||
|
|||
@Module({ |
|||
imports: [TypeOrmModule.forFeature([Role, RolesRepository])], |
|||
controllers: [RolesController], |
|||
providers: [RolesService], |
|||
exports: [RolesService], |
|||
}) |
|||
export class RolesModule {} |
|||
@ -0,0 +1,14 @@ |
|||
import { EntityRepository, Repository } from 'typeorm'; |
|||
import { Role } from './roles.entity'; |
|||
import { Optional } from 'typescript-optional'; |
|||
|
|||
@EntityRepository(Role) |
|||
export class RolesRepository extends Repository<Role> { |
|||
async findRoleByName(name: string): Promise<Optional<Role>> { |
|||
return Optional.ofNullable(await this.findOne({ name })); |
|||
} |
|||
|
|||
async findOneById(id: number): Promise<Optional<Role>> { |
|||
return Optional.ofNullable(await this.findOne(id, {})); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
import { Role } from './roles.entity'; |
|||
import { Injectable } from '@nestjs/common'; |
|||
import { InjectRepository } from '@nestjs/typeorm'; |
|||
import { RolesRepository } from './roles.repository'; |
|||
import { USER_ROLE, ADMIN_ROLE } from './roles.constants'; |
|||
import { Optional } from 'typescript-optional'; |
|||
|
|||
@Injectable() |
|||
export class RolesService { |
|||
constructor( |
|||
@InjectRepository(RolesRepository) |
|||
private readonly rolesRepository: RolesRepository, |
|||
) { |
|||
this.init(); |
|||
} |
|||
|
|||
async init(): Promise<void> { |
|||
for (const role of [USER_ROLE, ADMIN_ROLE]) { |
|||
if ((await this.rolesRepository.findRoleByName(role)).isEmpty()) { |
|||
const roleDb = new Role(role); |
|||
await this.rolesRepository.save(roleDb); |
|||
} |
|||
} |
|||
} |
|||
|
|||
async getAll(): Promise<Role[]> { |
|||
return this.rolesRepository.find({}); |
|||
} |
|||
|
|||
async getOneById(id: number): Promise<Optional<Role>> { |
|||
return this.rolesRepository.findOneById(id); |
|||
} |
|||
|
|||
async getUserRole(): Promise<Role> { |
|||
return (await this.rolesRepository.findRoleByName(USER_ROLE)).get(); |
|||
} |
|||
|
|||
async getAdminRole(): Promise<Role> { |
|||
return (await this.rolesRepository.findRoleByName(ADMIN_ROLE)).get(); |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
import { |
|||
Body, |
|||
Controller, |
|||
Delete, |
|||
ForbiddenException, |
|||
Get, |
|||
NotFoundException, |
|||
Param, |
|||
ParseIntPipe, |
|||
Post, |
|||
Put, |
|||
Query, |
|||
UseGuards, |
|||
} from '@nestjs/common'; |
|||
import { User } from './user.entity'; |
|||
import { UserService } from './user.service'; |
|||
import { |
|||
ApiBearerAuth, |
|||
ApiImplicitParam, |
|||
ApiResponse, |
|||
ApiUseTags, |
|||
} from '@nestjs/swagger'; |
|||
import { |
|||
UserDtoRegister, |
|||
UserDtoUpdateInfo, |
|||
UserDtoUpdatePassword, |
|||
} from './user.dto'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
import { Roles } from '../../decorators/roles.decorator'; |
|||
import { ADMIN_ROLE } from '../roles/roles.constants'; |
|||
import { CurrentUser } from '../../decorators/currentUser.decorator'; |
|||
|
|||
@ApiUseTags('User') |
|||
@Controller() |
|||
@ApiBearerAuth() |
|||
export class UserController { |
|||
constructor(private readonly userService: UserService) {} |
|||
|
|||
@Get() |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'Get a list of all User.', |
|||
type: User, |
|||
isArray: true, |
|||
}) |
|||
@Roles(ADMIN_ROLE) |
|||
getAll(): Promise<User[]> { |
|||
return this.userService.getAll(); |
|||
} |
|||
|
|||
@Post() |
|||
@ApiResponse({ |
|||
status: 201, |
|||
description: 'The User has been created.', |
|||
type: User, |
|||
}) |
|||
@Roles(ADMIN_ROLE) |
|||
saveNew(@Body() userDto: UserDtoRegister): Promise<User> { |
|||
return this.userService.saveNew(userDto); |
|||
} |
|||
|
|||
@Get(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The User with the matching id', |
|||
type: User, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async findOne( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
@CurrentUser() user: User, |
|||
): Promise<User> { |
|||
if (!user.isAdmin() && user.id !== id) { |
|||
throw new ForbiddenException(); |
|||
} |
|||
return (await this.userService.getOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
} |
|||
|
|||
@Put(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The updated User with the matching id', |
|||
type: User, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async updateOne( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
@Body() userDto: UserDtoUpdateInfo, |
|||
@CurrentUser() user: User, |
|||
): Promise<User> { |
|||
if (!user.isAdmin() && user.id !== id) { |
|||
throw new ForbiddenException(); |
|||
} |
|||
return this.userService.update(id, userDto); |
|||
} |
|||
|
|||
@Put(':id/password') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The updated User with the matching id', |
|||
type: User, |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async updateOnePassword( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
@Body() userDto: UserDtoUpdatePassword, |
|||
@CurrentUser() user: User, |
|||
): Promise<User> { |
|||
if (!user.isAdmin() && user.id !== id) { |
|||
throw new ForbiddenException(); |
|||
} |
|||
return this.userService.updatePassword(id, userDto); |
|||
} |
|||
|
|||
@Delete(':id') |
|||
@ApiResponse({ |
|||
status: 200, |
|||
description: 'The User with the matching id was deleted', |
|||
}) |
|||
@ApiResponse({ status: 404, description: 'Not found.' }) |
|||
async deleteOne( |
|||
@Param('id', new ParseIntPipe()) id: number, |
|||
@CurrentUser() user: User, |
|||
): Promise<void> { |
|||
if (!user.isAdmin() && user.id !== id) { |
|||
throw new ForbiddenException(); |
|||
} |
|||
await this.userService.deleteById(id); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import { |
|||
IsArray, |
|||
IsEmail, |
|||
IsOptional, |
|||
IsString, |
|||
Min, |
|||
MinLength, |
|||
} from 'class-validator'; |
|||
import { Type } from 'class-transformer'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
|
|||
export class UserDtoRegister { |
|||
@IsEmail() |
|||
@ApiModelProperty({ example: 'foo@bar.fr' }) |
|||
email: string; |
|||
|
|||
@IsString() |
|||
@ApiModelProperty({ example: 'foo' }) |
|||
firstName: string; |
|||
|
|||
@IsString() |
|||
@ApiModelProperty({ example: 'bar' }) |
|||
lastName: string; |
|||
|
|||
@IsString() |
|||
@MinLength(6) |
|||
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
|||
password: string; |
|||
} |
|||
|
|||
export class UserDtoUpdateInfo { |
|||
@IsString() |
|||
firstName: string; |
|||
|
|||
@IsString() |
|||
lastName: string; |
|||
} |
|||
|
|||
export class UserDtoUpdatePassword { |
|||
@IsString() |
|||
@MinLength(6) |
|||
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
|||
oldPassword: string; |
|||
|
|||
@IsString() |
|||
@MinLength(6) |
|||
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
|||
newPassword: string; |
|||
|
|||
@IsString() |
|||
@MinLength(6) |
|||
@ApiModelProperty({ example: 'azerty', minLength: 6 }) |
|||
newPasswordBis: string; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
import { DbAuditModel } from '../../utils/dbmodel.model'; |
|||
import { Column, Entity, JoinTable, ManyToMany } from 'typeorm'; |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
import { Exclude } from 'class-transformer'; |
|||
import { Role } from '../roles/roles.entity'; |
|||
import { ADMIN_ROLE } from '../roles/roles.constants'; |
|||
|
|||
@Entity() |
|||
export class User extends DbAuditModel { |
|||
@Column() |
|||
@ApiModelProperty({ example: 'foo@bar.fr' }) |
|||
email: string; |
|||
|
|||
@Column() |
|||
@ApiModelProperty() |
|||
firstName: string; |
|||
|
|||
@Column() |
|||
@ApiModelProperty() |
|||
lastName: string; |
|||
|
|||
@Column() |
|||
@Exclude() |
|||
password: string; |
|||
|
|||
@ManyToMany(type => Role) |
|||
@JoinTable() |
|||
roles: Role[]; |
|||
|
|||
isAdmin(): boolean { |
|||
return !!(this.roles || []).find(role => role.name === ADMIN_ROLE); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { TypeOrmModule } from '@nestjs/typeorm'; |
|||
import { UserController } from './user.controller'; |
|||
import { UserService } from './user.service'; |
|||
import { User } from './user.entity'; |
|||
import { UserRepository } from './user.repository'; |
|||
import { CryptoModule } from '../core/crypto/crypto.module'; |
|||
import { RolesModule } from '../roles/roles.module'; |
|||
|
|||
@Module({ |
|||
imports: [ |
|||
TypeOrmModule.forFeature([User, UserRepository]), |
|||
CryptoModule, |
|||
RolesModule, |
|||
], |
|||
controllers: [UserController], |
|||
providers: [UserService], |
|||
exports: [UserService], |
|||
}) |
|||
export class UserModule {} |
|||
@ -0,0 +1,22 @@ |
|||
import { EntityRepository, Repository } from 'typeorm'; |
|||
import { User } from './user.entity'; |
|||
import { Optional } from 'typescript-optional'; |
|||
|
|||
@EntityRepository(User) |
|||
export class UserRepository extends Repository<User> { |
|||
async findOneById(id: number): Promise<Optional<User>> { |
|||
return Optional.ofNullable( |
|||
await this.findOne(id, { relations: ['roles'] }), |
|||
); |
|||
} |
|||
|
|||
async hasUserWithMatchingEmail(email: string): Promise<boolean> { |
|||
return (await this.count({ where: { email } })) === 1; |
|||
} |
|||
|
|||
async findOneWithEmail(email: string): Promise<Optional<User>> { |
|||
return Optional.ofNullable( |
|||
await this.findOne({ email: email.toLowerCase() }), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
import { User } from './user.entity'; |
|||
import { |
|||
BadRequestException, |
|||
ConflictException, |
|||
Inject, |
|||
Injectable, |
|||
NotFoundException, |
|||
} from '@nestjs/common'; |
|||
import { InjectRepository } from '@nestjs/typeorm'; |
|||
import { UserRepository } from './user.repository'; |
|||
// import { } from './user.constants';
|
|||
import { |
|||
UserDtoRegister, |
|||
UserDtoUpdateInfo, |
|||
UserDtoUpdatePassword, |
|||
} from './user.dto'; |
|||
import { Optional } from 'typescript-optional'; |
|||
import { CryptoService } from '../core/crypto/crypto.service'; |
|||
import { Role } from '../roles/roles.entity'; |
|||
import { Roles } from '../../decorators/roles.decorator'; |
|||
import { USER_ROLE } from '../roles/roles.constants'; |
|||
import { RolesService } from '../roles/roles.service'; |
|||
|
|||
@Injectable() |
|||
export class UserService { |
|||
constructor( |
|||
@InjectRepository(UserRepository) |
|||
private readonly userRepository: UserRepository, |
|||
private readonly cryptoService: CryptoService, |
|||
private readonly rolesSercie: RolesService, |
|||
) {} |
|||
|
|||
async getAll(): Promise<User[]> { |
|||
return this.userRepository.find({}); |
|||
} |
|||
|
|||
async getOneById(id: number): Promise<Optional<User>> { |
|||
return this.userRepository.findOneById(id); |
|||
} |
|||
|
|||
async getOnWithEmail(email: string): Promise<Optional<User>> { |
|||
return await this.userRepository.findOneWithEmail(email); |
|||
} |
|||
|
|||
async doPasswordMatch(user: User, password: string): Promise<boolean> { |
|||
return this.cryptoService.compare(password, user.password); |
|||
} |
|||
|
|||
async saveNew(userRegister: UserDtoRegister): Promise<User> { |
|||
if ( |
|||
await this.userRepository.hasUserWithMatchingEmail( |
|||
userRegister.email.toLowerCase(), |
|||
) |
|||
) { |
|||
throw new ConflictException('Email already taken'); |
|||
} |
|||
|
|||
let userNew = new User(); |
|||
|
|||
const userRole = await this.rolesSercie.getUserRole(); |
|||
|
|||
userNew.password = await this.cryptoService.hash(userRegister.password); |
|||
userNew.email = userRegister.email.toLowerCase(); |
|||
userNew.lastName = userRegister.lastName; |
|||
userNew.firstName = userRegister.firstName; |
|||
userNew.roles = [userRole]; |
|||
|
|||
userNew = await this.userRepository.save(userNew); |
|||
|
|||
return userNew; |
|||
} |
|||
|
|||
async update(id: number, body: UserDtoUpdateInfo): Promise<User> { |
|||
let userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
|
|||
userFound.firstName = body.firstName; |
|||
userFound.lastName = body.lastName; |
|||
|
|||
userFound = await this.userRepository.save(userFound); |
|||
|
|||
return userFound; |
|||
} |
|||
|
|||
async updatePassword(id: number, body: UserDtoUpdatePassword): Promise<User> { |
|||
let userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
|
|||
if (!this.doPasswordMatch(userFound, body.oldPassword)) { |
|||
throw new BadRequestException('Old password do not match'); |
|||
} |
|||
|
|||
if (body.newPassword !== body.newPasswordBis) { |
|||
throw new BadRequestException('New passwords are not the same'); |
|||
} |
|||
|
|||
userFound.password = await this.cryptoService.hash(body.newPassword); |
|||
|
|||
userFound = await this.userRepository.save(userFound); |
|||
|
|||
return userFound; |
|||
} |
|||
|
|||
async deleteById(id: number): Promise<void> { |
|||
const userFound = (await this.userRepository.findOneById(id)).orElseThrow( |
|||
() => new NotFoundException(), |
|||
); |
|||
await this.userRepository.remove(userFound); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import { ApiModelProperty } from '@nestjs/swagger'; |
|||
import { |
|||
CreateDateColumn, |
|||
PrimaryGeneratedColumn, |
|||
UpdateDateColumn, |
|||
VersionColumn, |
|||
} from 'typeorm'; |
|||
import { Exclude } from 'class-transformer'; |
|||
|
|||
/** |
|||
* This model helps to have audits metrics to db models |
|||
*/ |
|||
export abstract class DbAuditModel { |
|||
@ApiModelProperty() |
|||
@PrimaryGeneratedColumn() |
|||
id: number; |
|||
|
|||
@CreateDateColumn() |
|||
@Exclude() |
|||
creationDate: Date; |
|||
|
|||
@UpdateDateColumn() |
|||
@Exclude() |
|||
updateDate: Date; |
|||
|
|||
@VersionColumn() |
|||
@Exclude() |
|||
version: number; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
import { INestApplication } from '@nestjs/common'; |
|||
import { Test } from '@nestjs/testing'; |
|||
import * as request from 'supertest'; |
|||
import { AppModule } from './../src/app.module'; |
|||
|
|||
describe('AppController (e2e)', () => { |
|||
let app: INestApplication; |
|||
|
|||
beforeAll(async () => { |
|||
const moduleFixture = await Test.createTestingModule({ |
|||
imports: [AppModule], |
|||
}).compile(); |
|||
|
|||
app = moduleFixture.createNestApplication(); |
|||
await app.init(); |
|||
}); |
|||
|
|||
it('/ping (GET)', () => { |
|||
return request(app.getHttpServer()) |
|||
.get('/ping') |
|||
.expect(200) |
|||
.expect('pong'); |
|||
}); |
|||
|
|||
afterAll(async () => { |
|||
await app.close(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"moduleFileExtensions": ["js", "json", "ts"], |
|||
"rootDir": ".", |
|||
"testEnvironment": "node", |
|||
"testRegex": ".e2e-spec.ts$", |
|||
"transform": { |
|||
"^.+\\.(t|j)s$": "ts-jest" |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue