├── .gitignore
├── .prettierrc
├── .vscode
└── launch.json
├── README.md
├── dev.env
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
├── __test__
│ ├── e2e
│ │ ├── config
│ │ │ ├── e2e.config.ts
│ │ │ ├── e2e.setup.ts
│ │ │ ├── e2e.teardown.ts
│ │ │ └── jest-e2e.json
│ │ ├── index.ts
│ │ ├── relations.e2e-spec.ts
│ │ ├── roles.e2e-spec.ts
│ │ └── users.e2e-spec.ts
│ ├── logging.interceptor.spec.ts
│ └── object-id.validation.pipe.spec.ts
├── app.module.ts
├── auth
│ ├── __test__
│ │ ├── auth.guard.spec.ts
│ │ └── auth.middleware.spec.ts
│ ├── auth.guard.ts
│ ├── auth.middleware.ts
│ ├── auth.roles.decorator.ts
│ └── index.ts
├── common.ts
├── config
│ ├── config.module.ts
│ ├── config.service.ts
│ └── index.ts
├── constants.ts
├── database
│ ├── database.module.ts
│ ├── database.providers.ts
│ └── index.ts
├── logging.interceptor.ts
├── main.ts
├── object-id.validation.pipe.ts
├── roles
│ ├── __test__
│ │ ├── mock.data.ts
│ │ └── roles.service.spec.ts
│ ├── dto
│ │ └── role.dto.ts
│ ├── errors
│ │ └── errors.ts
│ ├── index.ts
│ ├── interfaces
│ │ └── interfaces.ts
│ ├── models
│ │ └── role.model.ts
│ ├── roles.controller.ts
│ ├── roles.module.ts
│ ├── roles.repository.ts
│ ├── roles.service.ts
│ └── schema
│ │ └── role.schema.ts
├── user-role-rel
│ ├── __test__
│ │ ├── mock.data.ts
│ │ └── user-role-rel.service.spec.ts
│ ├── dto
│ │ └── user-role-rel.dto.ts
│ ├── errors
│ │ └── errors.ts
│ ├── index.ts
│ ├── interfaces
│ │ └── interfaces.ts
│ ├── models
│ │ └── user-role-rel.model.ts
│ ├── schema
│ │ └── user-role-rel.schema.ts
│ ├── user-role-rel.controller.ts
│ ├── user-role-rel.module.ts
│ └── user-role-rel.service.ts
└── users
│ ├── __test__
│ ├── mock.data.ts
│ ├── users.repository.spec.ts
│ └── users.service.spec.ts
│ ├── dto
│ └── user.dto.ts
│ ├── errors
│ └── errors.ts
│ ├── index.ts
│ ├── interfaces
│ └── interfaces.ts
│ ├── models
│ └── user.model.ts
│ ├── schema
│ └── user.schema.ts
│ ├── users.controller.ts
│ ├── users.module.ts
│ ├── users.repository.ts
│ └── users.service.ts
├── test.env
├── tsconfig.build.json
├── tsconfig.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "tabWidth": 2,
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch with transpilation",
9 | "type": "node",
10 | "request": "launch",
11 | "args": ["${workspaceFolder}/src/main.ts"],
12 | "env": {
13 | "TS_NODE_TRANSPILE_ONLY": "true"
14 | },
15 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
16 | "sourceMaps": true,
17 | "cwd": "${workspaceRoot}",
18 | "protocol": "inspector"
19 | },
20 | {
21 | "type": "node",
22 | "request": "launch",
23 | "name": "Debug app",
24 | "program": "${workspaceFolder}/dist/src/main.js"
25 | },
26 | {
27 | "type": "node",
28 | "request": "launch",
29 | "name": "Jest File",
30 | "program": "${workspaceFolder}/node_modules/jest/bin/jest",
31 | "args": [
32 | "--runInBand",
33 | "${fileBasenameNoExtension}",
34 | "--config=${workspaceFolder}/package.json",
35 | "--coverage=false"
36 | ],
37 | "console": "internalConsole",
38 | "internalConsoleOptions": "neverOpen",
39 | "disableOptimisticBPs": true,
40 | "windows": {
41 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
42 | }
43 | },
44 | {
45 | "type": "node",
46 | "request": "launch",
47 | "name": "Jest File (coverage)",
48 | "program": "${workspaceFolder}/node_modules/jest/bin/jest",
49 | "args": [
50 | "--runInBand",
51 | "${fileBasenameNoExtension}",
52 | "--config=${workspaceFolder}/package.json"
53 | ],
54 | "console": "internalConsole",
55 | "internalConsoleOptions": "neverOpen",
56 | "disableOptimisticBPs": true,
57 | "windows": {
58 | "program": "${workspaceFolder}/node_modules/jest/bin/jest"
59 | }
60 | }
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 |
6 |
7 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
8 | [travis-url]: https://travis-ci.org/nestjs/nest
9 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
10 | [linux-url]: https://travis-ci.org/nestjs/nest
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | A simple CRUD app built with Nest.js framework and MongoDB.
25 |
26 | Before running the app ensure you have installed Node.js and MongoDB.
27 |
28 | ## Installation
29 |
30 | ```bash
31 | $ npm install
32 | ```
33 |
34 | ## Build
35 |
36 | ```bash
37 | $ npm run build
38 | ```
39 |
40 | ## Running the app
41 |
42 | ```bash
43 | $ npm run start
44 | ```
45 |
46 | ## Tests
47 |
48 | ```bash
49 | # unit tests
50 | $ npm run test
51 |
52 | # e2e tests
53 | $ npm run test:e2e
54 | ```
55 |
56 | ## License
57 |
58 | Nest is [MIT licensed](LICENSE).
59 |
--------------------------------------------------------------------------------
/dev.env:
--------------------------------------------------------------------------------
1 | MONGO_URL = mongodb://localhost:27017/nestjs-test-dev
2 | NODE_ENV = dev
3 | PORT = 3000
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "ts",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-example-app",
3 | "version": "0.0.1",
4 | "description": "A sample nest js project for self training purpose",
5 | "author": "Marat Sadykov",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "ts-node -r tsconfig-paths/register src/main.ts",
10 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/src/main.js\"",
11 | "lint": "tslint -p tsconfig.json -c tslint.json",
12 | "test": "jest --runInBand --verbose",
13 | "test:e2e": "jest --runInBand --forceExit --verbose --config src/__test__/e2e/config/jest-e2e.json",
14 | "precommit": "npm run lint && npm run build && npm run test"
15 | },
16 | "dependencies": {
17 | "@hapi/joi": "^17.1.1",
18 | "@nestjs/common": "^6.11.11",
19 | "@nestjs/core": "^6.11.11",
20 | "@nestjs/mongoose": "^6.4.0",
21 | "@nestjs/platform-express": "^6.11.11",
22 | "@nestjs/swagger": "^4.4.0",
23 | "@nestjs/testing": "^6.11.11",
24 | "class-transformer": "^0.3.1",
25 | "class-validator": "^0.11.1",
26 | "dotenv": "^8.2.0",
27 | "express": "^4.17.1",
28 | "faker": "^4.1.0",
29 | "monet": "^0.9.1",
30 | "mongoose": "^5.9.5",
31 | "reflect-metadata": "^0.1.13",
32 | "request-promise-native": "^1.0.8",
33 | "rimraf": "^3.0.2",
34 | "rxjs": "^6.5.4",
35 | "swagger-ui-express": "^4.1.4",
36 | "winston": "^3.2.1"
37 | },
38 | "devDependencies": {
39 | "@types/dotenv": "^8.2.0",
40 | "@types/express": "4.17.2",
41 | "@types/faker": "^4.1.10",
42 | "@types/hapi__joi": "^16.0.12",
43 | "@types/jest": "25.1.3",
44 | "@types/mongoose": "^5.7.7",
45 | "@types/node": "^13.9.3",
46 | "@types/request-promise-native": "^1.0.17",
47 | "@types/supertest": "2.0.8",
48 | "jest": "25.1.0",
49 | "mongodb-memory-server": "^6.4.1",
50 | "prettier": "1.19.1",
51 | "supertest": "4.0.2",
52 | "ts-jest": "25.2.1",
53 | "ts-node": "8.6.2",
54 | "tsc-watch": "4.2.2",
55 | "tsconfig-paths": "3.9.0",
56 | "tslint": "6.0.0",
57 | "typescript": "^3.8.3"
58 | },
59 | "jest": {
60 | "moduleFileExtensions": [
61 | "js",
62 | "json",
63 | "ts"
64 | ],
65 | "rootDir": "src",
66 | "testRegex": ".spec.ts$",
67 | "transform": {
68 | "^.+\\.(t|j)s$": "ts-jest"
69 | },
70 | "coverageDirectory": "../coverage",
71 | "testEnvironment": "node",
72 | "testPathIgnorePatterns": [
73 | "/__test__/e2e"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/__test__/e2e/config/e2e.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 |
3 | config({ path: 'test.env' });
4 | const e2eConfig = {
5 | PORT: process.env.PORT,
6 | MONGO_URL: process.env.MONGO_URL,
7 | URL: process.env.URL,
8 | };
9 |
10 | export { e2eConfig };
11 |
--------------------------------------------------------------------------------
/src/__test__/e2e/config/e2e.setup.ts:
--------------------------------------------------------------------------------
1 | import { bootstrap } from '../../../main';
2 | import './e2e.config';
3 | import { getMockUsers } from '../../../users/__test__/mock.data';
4 |
5 | module.exports = async () => {
6 | Object.assign(process.env, { NODE_ENV: 'test' });
7 | (global as any).__APP__ = await bootstrap();
8 |
9 | await getMockUsers();
10 | };
11 |
--------------------------------------------------------------------------------
/src/__test__/e2e/config/e2e.teardown.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 |
3 | module.exports = async () => {
4 | try {
5 | await mongoose.connection.db.dropDatabase();
6 | await (global as any).__APP__.close();
7 | } catch (exception) {
8 | console.log('Exception: ', exception);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/__test__/e2e/config/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["ts", "js", "json"],
3 | "rootDir": "../",
4 | "testRegex": "(.spec.ts$)",
5 | "testEnvironment": "node",
6 | "globalSetup": "./config/e2e.setup.ts",
7 | "globalTeardown": "./config/e2e.teardown.ts",
8 | "globals": {
9 | "ts-jest": {
10 | "tsConfig": "./tsconfig.json",
11 | "diagnostics": false,
12 | "isolatedModules": true
13 | }
14 | },
15 | "transform": {
16 | "^.+\\.ts$": "ts-jest"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/__test__/e2e/index.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'request-promise-native';
2 | import { ConfigService } from '../../config';
3 | type Options = request.Options;
4 | type Response = request.FullResponse;
5 |
6 | const config = new ConfigService(process.env.NODE_ENV + '.env');
7 | const url = config.get('URL');
8 | const port = config.get('PORT');
9 |
10 | export const authenticatedRequest = async (options: Options): Promise =>
11 | request({
12 | baseUrl: `${url}:${port}`,
13 | // use admin credentials to perform all api requests
14 | auth: {
15 | username: 'admin',
16 | password: '123',
17 | },
18 | json: true,
19 | simple: false,
20 | resolveWithFullResponse: true,
21 | ...options,
22 | });
23 |
--------------------------------------------------------------------------------
/src/__test__/e2e/relations.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { authenticatedRequest } from '.';
2 | import './config/e2e.setup';
3 | import { UserRoleRelation } from '../../user-role-rel';
4 | import { sample, ObjectID } from '../../common';
5 |
6 | describe('User role relations: E2E', () => {
7 | let mockRelations: UserRoleRelation[];
8 |
9 | beforeAll(async () => {
10 | const response = await authenticatedRequest({
11 | method: 'GET',
12 | url: '/api/relations',
13 | });
14 | mockRelations = response.body;
15 | });
16 |
17 | describe('GET', () => {
18 | it('should get all relations', async () => {
19 | const response = await authenticatedRequest({
20 | method: 'GET',
21 | url: '/api/relations',
22 | });
23 | const relations = response.body;
24 |
25 | expect(response.statusCode).toBe(200);
26 | expect(relations.length).toBeGreaterThan(0);
27 | });
28 |
29 | it('should get role relations assigned to user by userId', async () => {
30 | const userId = sample(mockRelations).userId;
31 | const response = await authenticatedRequest({
32 | method: 'GET',
33 | url: `/api/relations/user?userId=${userId}`,
34 | });
35 | const relations: UserRoleRelation[] = response.body;
36 |
37 | expect(response.statusCode).toBe(200);
38 | relations.every(relation => {
39 | expect(relation.userId).toBe(userId);
40 | });
41 | });
42 |
43 | it('should return 404 status if role relations assigned to user not found', async () => {
44 | const userId = new ObjectID().toHexString();
45 | const response = await authenticatedRequest({
46 | method: 'GET',
47 | url: `/api/relations/user?userId=${userId}`,
48 | });
49 |
50 | expect(response.statusCode).toBe(404);
51 | expect(response.body).toMatchObject({
52 | status: 404,
53 | error: 'RoleRelationNotFoundError',
54 | message: `Role relation to user ${userId} not found`,
55 | });
56 | });
57 | });
58 |
59 | describe('CREATE', () => {
60 | it('should assign role to user', async () => {
61 | const relation = {
62 | userId: new ObjectID().toHexString(),
63 | roleId: new ObjectID().toHexString(),
64 | };
65 |
66 | const response = await authenticatedRequest({
67 | method: 'POST',
68 | url: '/api/relations',
69 | body: relation,
70 | });
71 |
72 | expect(response.statusCode).toBe(201);
73 | expect(response.body.userId).toBe(relation.userId);
74 | expect(response.body.roleId).toBe(relation.roleId);
75 | });
76 |
77 | it('should return 409 status if role relation already exists', async () => {
78 | const existingRelation = sample(mockRelations);
79 | const response = await authenticatedRequest({
80 | method: 'POST',
81 | url: '/api/relations',
82 | body: existingRelation,
83 | });
84 |
85 | expect(response.statusCode).toBe(409);
86 | expect(response.body).toMatchObject({
87 | status: 409,
88 | error: 'RoleRelationAlreadyExistsError',
89 | message: `User role relation with role id:${existingRelation.roleId} already exists`,
90 | });
91 | });
92 | });
93 |
94 | describe('DELETE', () => {
95 | it('should delete specific relation by id', async () => {
96 | const id = sample(mockRelations).id;
97 | const response = await authenticatedRequest({
98 | method: 'DELETE',
99 | url: `/api/relations/${id}`,
100 | });
101 |
102 | expect(response.statusCode).toBe(200);
103 | expect(response.body.isDeleted).toBe(true);
104 | });
105 |
106 | it('should return 404 status if relation for delete not found', async () => {
107 | const id = new ObjectID().toHexString();
108 | const response = await authenticatedRequest({
109 | method: 'DELETE',
110 | url: `/api/relations/${id}`,
111 | });
112 |
113 | expect(response.statusCode).toBe(404);
114 | expect(response.body).toMatchObject({
115 | status: 404,
116 | error: 'RelationNotFoundError',
117 | message: `Relation ${id} not found`,
118 | });
119 | });
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/src/__test__/e2e/roles.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { authenticatedRequest } from '.';
2 | import './config/e2e.setup';
3 | import { Role, RoleForCreate } from '../../roles';
4 | import { sample, ObjectID } from '../../common';
5 | import * as faker from 'faker';
6 |
7 | describe('Roles: E2E', () => {
8 | let mockRoles: Role[];
9 |
10 | beforeAll(async () => {
11 | const result = await authenticatedRequest({
12 | method: 'GET',
13 | url: '/api/roles',
14 | });
15 | mockRoles = result.body;
16 | });
17 |
18 | describe('GET', () => {
19 | it('should get all roles', async () => {
20 | const response = await authenticatedRequest({
21 | method: 'GET',
22 | url: '/api/roles',
23 | });
24 | const roles: Role[] = response.body;
25 |
26 | expect(response.statusCode).toBe(200);
27 | expect(roles.length).toBeGreaterThan(0);
28 | });
29 |
30 | it('should get specific role by id', async () => {
31 | const id = sample(mockRoles).id;
32 | const response = await authenticatedRequest({
33 | method: 'GET',
34 | url: `/api/roles/${id}`,
35 | });
36 |
37 | expect(response.statusCode).toBe(200);
38 | expect(response.body.id).toBe(id);
39 | });
40 |
41 | it('should return 404 if role id not found', async () => {
42 | const id = new ObjectID().toHexString();
43 | const response = await authenticatedRequest({
44 | method: 'GET',
45 | url: `/api/roles/${id}`,
46 | });
47 |
48 | expect(response.statusCode).toBe(404);
49 | expect(response.body).toMatchObject({
50 | status: 404,
51 | error: 'RoleNotFoundError',
52 | message: `Role id:${id} not found`,
53 | });
54 | });
55 | });
56 |
57 | describe('CREATE', () => {
58 | it('should create new role', async () => {
59 | const role: RoleForCreate = {
60 | name: faker.lorem.word(),
61 | displayName: faker.lorem.word().toUpperCase(),
62 | description: faker.lorem.sentence(),
63 | };
64 |
65 | const response = await authenticatedRequest({
66 | method: 'POST',
67 | url: '/api/roles',
68 | body: role,
69 | });
70 |
71 | expect(response.statusCode).toBe(201);
72 | expect(response.body.name).toBe(role.name);
73 | expect(response.body.displayName).toBe(role.displayName);
74 | expect(response.body.description).toBe(role.description);
75 | });
76 |
77 | it('should return 409 if role already exists', async () => {
78 | const existingRole = sample(mockRoles);
79 | const response = await authenticatedRequest({
80 | method: 'POST',
81 | url: '/api/roles',
82 | body: existingRole,
83 | });
84 |
85 | expect(response.statusCode).toBe(409);
86 | expect(response.body).toMatchObject({
87 | status: 409,
88 | error: 'RoleAlreadyExistsError',
89 | message: `Role name:${existingRole.name} already exists`,
90 | });
91 | });
92 | });
93 |
94 | describe('PATCH', () => {
95 | it('should update specific role by id', async () => {
96 | const id = sample(mockRoles).id;
97 | const patch = {
98 | name: faker.lorem.word(),
99 | displayName: faker.lorem.word().toUpperCase(),
100 | };
101 |
102 | const response = await authenticatedRequest({
103 | method: 'PATCH',
104 | url: `/api/roles/${id}`,
105 | body: patch,
106 | });
107 |
108 | expect(response.statusCode).toBe(200);
109 | expect(response.body.name).toBe(patch.name);
110 | expect(response.body.displayName).toBe(patch.displayName);
111 | });
112 |
113 | it('should return 404 status if role for update not found', async () => {
114 | const id = new ObjectID().toHexString();
115 | const patch = {
116 | name: faker.lorem.word(),
117 | displayName: faker.lorem.word().toUpperCase(),
118 | };
119 |
120 | const response = await authenticatedRequest({
121 | method: 'PATCH',
122 | url: `/api/roles/${id}`,
123 | body: patch,
124 | });
125 |
126 | expect(response.statusCode).toBe(404);
127 | expect(response.body).toMatchObject({
128 | status: 404,
129 | error: 'RoleNotFoundError',
130 | message: `Role id:${id} not found`,
131 | });
132 | });
133 | });
134 |
135 | describe('DELETE', () => {
136 | it('should delete specific role by id', async () => {
137 | const id = sample(mockRoles).id;
138 | const response = await authenticatedRequest({
139 | method: 'DELETE',
140 | url: `/api/roles/${id}`,
141 | });
142 |
143 | expect(response.statusCode).toBe(200);
144 | expect(response.body.isDeleted).toBe(true);
145 | });
146 |
147 | it('should return 404 if role for delete not found', async () => {
148 | const id = new ObjectID().toHexString();
149 | const response = await authenticatedRequest({
150 | method: 'DELETE',
151 | url: `/api/roles/${id}`,
152 | });
153 |
154 | expect(response.statusCode).toBe(404);
155 | expect(response.body).toMatchObject({
156 | status: 404,
157 | error: 'RoleNotFoundError',
158 | message: `Role id:${id} not found`,
159 | });
160 | });
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/__test__/e2e/users.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { authenticatedRequest } from '.';
2 | import './config/e2e.setup';
3 | import { User } from '../../users';
4 | import { sample, ObjectID } from '../../common';
5 | import * as faker from 'faker';
6 |
7 | faker.seed(65);
8 |
9 | describe('Users: E2E', () => {
10 | let mockUsers: User[];
11 |
12 | beforeAll(async () => {
13 | const result = await authenticatedRequest({
14 | method: 'GET',
15 | url: '/api/users',
16 | });
17 | mockUsers = result.body;
18 | });
19 |
20 | describe('GET', () => {
21 | it('should get all users', async () => {
22 | const response = await authenticatedRequest({
23 | method: 'GET',
24 | url: '/api/users',
25 | });
26 | const users: User[] = response.body;
27 |
28 | expect(response.statusCode).toBe(200);
29 | expect(users.length).toBeGreaterThan(0);
30 | });
31 |
32 | it('should get specific user by id', async () => {
33 | const id = sample(mockUsers).id;
34 | const response = await authenticatedRequest({
35 | method: 'GET',
36 | url: `/api/users/${id}`,
37 | });
38 |
39 | expect(response.statusCode).toBe(200);
40 | expect(response.body.id).toBe(id);
41 | });
42 |
43 | it('should return 404 status if user id not found', async () => {
44 | const id = new ObjectID().toHexString();
45 |
46 | const response = await authenticatedRequest({
47 | method: 'GET',
48 | url: `/api/users/${id}`,
49 | });
50 |
51 | expect(response.statusCode).toBe(404);
52 | expect(response.body).toMatchObject({
53 | status: 404,
54 | error: 'UserNotFoundError',
55 | message: `User ${id} not found`,
56 | });
57 | });
58 | });
59 |
60 | describe('CREATE', () => {
61 | it('should create new user', async () => {
62 | const user = {
63 | username: faker.internet.userName(),
64 | password: faker.internet.password(),
65 | };
66 |
67 | const response = await authenticatedRequest({
68 | method: 'POST',
69 | url: `/api/users/`,
70 | body: user,
71 | });
72 |
73 | expect(response.statusCode).toBe(201);
74 | expect(response.body.username).toBe(user.username);
75 | });
76 |
77 | it('should return 409 status if user already exists', async () => {
78 | const existingUser = sample(mockUsers);
79 |
80 | const response = await authenticatedRequest({
81 | method: 'POST',
82 | url: '/api/users/',
83 | body: existingUser,
84 | });
85 |
86 | expect(response.statusCode).toBe(409);
87 | expect(response.body).toMatchObject({
88 | status: 409,
89 | error: 'UserAlreadyExistsError',
90 | message: `User ${existingUser.username} already exists`,
91 | });
92 | });
93 | });
94 |
95 | describe('PATCH', () => {
96 | it('should update specific user by id', async () => {
97 | const id = sample(mockUsers).id;
98 | const patch = {
99 | username: faker.internet.userName(),
100 | };
101 |
102 | const response = await authenticatedRequest({
103 | method: 'PATCH',
104 | url: `/api/users/${id}`,
105 | body: patch,
106 | });
107 |
108 | expect(response.statusCode).toBe(200);
109 | expect(response.body.id).toBe(id);
110 | expect(response.body.username).toBe(patch.username);
111 | });
112 |
113 | it('should return 404 status if user for update not found', async () => {
114 | const id = new ObjectID().toHexString();
115 | const patch = {
116 | username: faker.internet.userName(),
117 | password: faker.internet.password(),
118 | };
119 |
120 | const response = await authenticatedRequest({
121 | method: 'PATCH',
122 | url: `/api/users/${id}`,
123 | body: patch,
124 | });
125 |
126 | expect(response.statusCode).toBe(404);
127 | expect(response.body).toMatchObject({
128 | status: 404,
129 | error: 'UserNotFoundError',
130 | message: `User ${id} not found`,
131 | });
132 | });
133 | });
134 |
135 | describe('DELETE', () => {
136 | it('should delete specific user by id', async () => {
137 | const id = sample(mockUsers).id;
138 | const response = await authenticatedRequest({
139 | method: 'DELETE',
140 | url: `/api/users/${id}`,
141 | });
142 |
143 | expect(response.statusCode).toBe(200);
144 | expect(response.body.isDeleted).toBe(true);
145 | });
146 |
147 | it('should return 404 status if user for delete not found', async () => {
148 | const id = new ObjectID().toHexString();
149 | const response = await authenticatedRequest({
150 | method: 'DELETE',
151 | url: `/api/users/${id}`,
152 | });
153 |
154 | expect(response.statusCode).toBe(404);
155 | expect(response.body).toMatchObject({
156 | status: 404,
157 | error: 'UserNotFoundError',
158 | message: `User ${id} not found`,
159 | });
160 | });
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/__test__/logging.interceptor.spec.ts:
--------------------------------------------------------------------------------
1 | import { LoggingInterceptor } from './../logging.interceptor';
2 | import { createLogger } from 'winston';
3 | import * as faker from 'faker';
4 | import { throwError } from 'rxjs';
5 |
6 | describe('Logging interceptor', () => {
7 | const request = { url: faker.internet.url(), method: 'PUT' };
8 | const context: any = {
9 | getHandler: jest.fn(),
10 | switchToHttp: () => ({ getRequest: () => request }),
11 | };
12 |
13 | const logger = createLogger({ silent: true });
14 | const interceptor = new LoggingInterceptor(logger);
15 | const log = jest.spyOn(logger, 'error');
16 |
17 | it('should log error and throw internal server error in case of exception', async () => {
18 | const errorMessage = 'Some error happened';
19 | const next: any = {
20 | handle: () => throwError(new Error(errorMessage)), // unhandled exception
21 | };
22 |
23 | interceptor.intercept(context, next).subscribe({
24 | error(err) {
25 | expect(err.response.statusCode).toBe(500);
26 | expect(err.response.message).toBe('Internal Server Error');
27 | },
28 | });
29 |
30 | const logData: any = log.mock.calls[0][0];
31 |
32 | expect(logData.url).toBe(request.url);
33 | expect(logData.method).toBe(request.method);
34 | expect(logData.message).toBe(errorMessage);
35 | expect(logData.stack).toBeTruthy();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/__test__/object-id.validation.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { ObjectIdValidationPipe } from './../object-id.validation.pipe';
2 | import { ObjectID } from './../common';
3 |
4 | function sample(list: Array) {
5 | return list[Math.floor(Math.random() * list.length)];
6 | }
7 |
8 | describe('ObjectId validation pipe', () => {
9 | const pipe = new ObjectIdValidationPipe();
10 |
11 | it('should return valid object id', () => {
12 | const id = new ObjectID().toHexString();
13 | const result = pipe.transform(id);
14 |
15 | expect(result).toBe(id);
16 | });
17 |
18 | it('should throw BadRequestException in case of invalid object id', async () => {
19 | try {
20 | const ids = [
21 | '82defcf324571e70b0521d79cce2bf3fffccd69',
22 | 'c1a050a4cd1556948d41',
23 | 'zzzzzzzzzzzz',
24 | 'South Africa',
25 | ];
26 | const invalidId = sample(ids);
27 |
28 | await pipe.transform(invalidId);
29 | } catch (exception) {
30 | expect(exception.response.statusCode).toBe(400);
31 | expect(exception.response.message).toBe('ObjectId cast error. Invalid id');
32 | }
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module } from '@nestjs/common';
2 | import { Authenticate } from './auth';
3 | import { ConfigModule } from './config';
4 | import { DatabaseModule } from './database';
5 | import { IRolesService, RolesModule } from './roles';
6 | import { IUserRoleRelService, UserRoleRelModule } from './user-role-rel';
7 | import { IUsersService, UsersModule } from './users';
8 |
9 | @Module({})
10 | export class AppModule {
11 | static async forRoot(dependencies: {
12 | usersService: IUsersService;
13 | rolesService: IRolesService;
14 | userRoleRelService: IUserRoleRelService;
15 | authenticate: Authenticate;
16 | }): Promise {
17 | return {
18 | module: AppModule,
19 | imports: [
20 | ConfigModule,
21 | DatabaseModule,
22 | RolesModule.forRoot(dependencies),
23 | UsersModule.forRoot(dependencies),
24 | UserRoleRelModule.forRoot(dependencies),
25 | ],
26 | };
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/auth/__test__/auth.guard.spec.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import { AuthGuard } from '../auth.guard';
3 | import { AuthenticatedRequest, authenticate as _authenticate } from '../auth.middleware';
4 | import { Right } from 'monet';
5 |
6 | describe('Auth Guard', () => {
7 | const reflector = {
8 | get: jest.fn(),
9 | };
10 | const authGuard = new AuthGuard(reflector as any);
11 | const userService = {
12 | getUserCredentials: jest.fn(),
13 | };
14 | const authenticate = _authenticate(userService as any);
15 | const response = {};
16 | const next = jest.fn();
17 | const context = {
18 | getHandler: jest.fn(),
19 | switchToHttp: jest.fn(),
20 | };
21 | const roles = ['admin', 'guest'];
22 |
23 | let mockReflectorSpy: any = jest.fn();
24 |
25 | async function mockAuthenticateUserImplementationOnce(mockUser: any) {
26 | const request: AuthenticatedRequest = {
27 | headers: {
28 | authorization: 'Basic ' + Buffer.from('username:password').toString('base64'),
29 | },
30 | };
31 |
32 | mockReflectorSpy = jest.spyOn(reflector, 'get').mockReturnValueOnce(roles);
33 |
34 | context.switchToHttp.mockImplementationOnce(() => {
35 | return {
36 | getRequest: () => request,
37 | };
38 | });
39 |
40 | userService.getUserCredentials.mockResolvedValueOnce(Promise.resolve(Right(mockUser)));
41 |
42 | await authenticate(request, response as any, next);
43 | }
44 |
45 | it('should return true if user has roles assigned', async () => {
46 | const user = {
47 | username: 'username',
48 | roles: ['admin'],
49 | };
50 |
51 | await mockAuthenticateUserImplementationOnce(user);
52 | const canActivate = authGuard.canActivate(context as any);
53 |
54 | expect(canActivate).toBe(true);
55 | mockReflectorSpy.mockReset();
56 | });
57 |
58 | it('should return false if user has no roles assigned', async () => {
59 | const user = {
60 | username: 'username',
61 | roles: [],
62 | };
63 |
64 | await mockAuthenticateUserImplementationOnce(user);
65 | const canActivate = authGuard.canActivate(context as any);
66 |
67 | expect(canActivate).toBe(false);
68 | mockReflectorSpy.mockReset();
69 | });
70 |
71 | it('should return false if user has invalid roles assigned', async () => {
72 | const user = {
73 | username: 'username',
74 | roles: ['non_existing_role'],
75 | };
76 |
77 | await mockAuthenticateUserImplementationOnce(user);
78 | const canActivate = authGuard.canActivate(context as any);
79 |
80 | expect(canActivate).toBe(false);
81 | mockReflectorSpy.mockReset();
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/auth/__test__/auth.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import { authenticate as _authenticate, AuthenticatedRequest } from '../auth.middleware';
3 | import { Right, Left } from 'monet';
4 |
5 | describe('Auth middleware', () => {
6 | const userService = {
7 | getUserCredentials: jest.fn(),
8 | };
9 | const authenticate = _authenticate(userService as any);
10 | const response = {};
11 | const next = jest.fn();
12 |
13 | it('should modify request globally with authenticated user', async () => {
14 | const request: AuthenticatedRequest = {
15 | headers: {
16 | authorization: 'Basic ' + Buffer.from(`username:password`).toString('base64'),
17 | },
18 | };
19 |
20 | jest.spyOn(userService, 'getUserCredentials').mockImplementationOnce(async () =>
21 | Right(
22 | (request.user = {
23 | username: 'username',
24 | roles: ['role'],
25 | }),
26 | ),
27 | );
28 |
29 | await authenticate(request, response as any, next);
30 |
31 | expect(request.user).toMatchObject({
32 | username: 'username',
33 | roles: ['role'],
34 | });
35 | });
36 |
37 | it('should throw Unauthorized exception if user has no authorization headers', async () => {
38 | const request: AuthenticatedRequest = {
39 | headers: {},
40 | };
41 |
42 | jest.spyOn(userService, 'getUserCredentials').mockImplementationOnce(async () =>
43 | Right(
44 | (request.user = {
45 | username: 'username',
46 | roles: ['role'],
47 | }),
48 | ),
49 | );
50 |
51 | try {
52 | await authenticate(request, response as any, next);
53 | } catch (error) {
54 | expect(error.status).toBe(401);
55 | expect(error.response).toMatchObject({
56 | statusCode: 401,
57 | error: 'Unauthorized',
58 | });
59 | }
60 | });
61 |
62 | it('should throw Unauthorized exception if user provided wrong login or password', async () => {
63 | const request: AuthenticatedRequest = {
64 | headers: {
65 | authorization: 'Basic ' + Buffer.from(`wronguser:wrongpassword`).toString('base64'),
66 | },
67 | };
68 |
69 | jest.spyOn(userService, 'getUserCredentials').mockImplementationOnce(async () =>
70 | Left(
71 | (request.user = {
72 | username: 'username',
73 | roles: ['role'],
74 | }),
75 | ),
76 | );
77 |
78 | try {
79 | await authenticate(request, response as any, next);
80 | } catch (error) {
81 | expect(error.status).toBe(401);
82 | expect(error.response).toMatchObject({
83 | statusCode: 401,
84 | error: 'Unauthorized',
85 | });
86 | }
87 | });
88 |
89 | it('should throw Unauthorized exception if user does not exists in system', async () => {
90 | const request: AuthenticatedRequest = {
91 | headers: {
92 | authorization: 'Basic ' + Buffer.from(`notexists:pwd`).toString('base64'),
93 | },
94 | };
95 |
96 | jest.spyOn(userService, 'getUserCredentials').mockImplementationOnce(async () =>
97 | Left(
98 | (request.user = {
99 | username: 'username',
100 | roles: ['role'],
101 | }),
102 | ),
103 | );
104 |
105 | try {
106 | await authenticate(request, response as any, next);
107 | } catch (error) {
108 | expect(error.status).toBe(401);
109 | expect(error.response).toMatchObject({
110 | statusCode: 401,
111 | error: 'Unauthorized',
112 | });
113 | }
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/src/auth/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
2 | import { Reflector } from '@nestjs/core';
3 |
4 | @Injectable()
5 | export class AuthGuard implements CanActivate {
6 | constructor(private readonly reflector: Reflector) {}
7 |
8 | canActivate(context: ExecutionContext): boolean {
9 | const roles = this.reflector.get('roles', context.getHandler());
10 |
11 | if (!roles) {
12 | return false;
13 | }
14 |
15 | const request = context.switchToHttp().getRequest();
16 | const user = request.user;
17 |
18 | if (!user) {
19 | return false;
20 | }
21 |
22 | return user.roles.some((role: string) => roles.includes(role));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/auth/auth.middleware.ts:
--------------------------------------------------------------------------------
1 | import { UnauthorizedException } from '@nestjs/common';
2 | import { Response, NextFunction } from 'express';
3 | import { IUsersService } from '../users/';
4 |
5 | export type AuthenticatedRequest = {
6 | [key: string]: any;
7 | };
8 |
9 | export type Authenticate = (
10 | req: AuthenticatedRequest,
11 | _: Response,
12 | next: NextFunction,
13 | ) => Promise;
14 |
15 | export function authenticate(usersService: IUsersService): Authenticate {
16 | return async (req, _, next) => {
17 | const authHeaders = req.headers.authorization;
18 |
19 | if (!authHeaders) {
20 | throw new UnauthorizedException();
21 | }
22 |
23 | const [username, password] = getAuthorizationCredentials(authHeaders);
24 | const user = await usersService.getUserCredentials(username, password);
25 |
26 | if (user.isLeft()) {
27 | throw new UnauthorizedException();
28 | }
29 |
30 | req.user = user.right();
31 |
32 | next();
33 | };
34 | }
35 |
36 | function getAuthorizationCredentials(header: string) {
37 | const base64Credentials = header.split(' ')[1];
38 | const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
39 |
40 | return credentials.split(':');
41 | }
42 |
--------------------------------------------------------------------------------
/src/auth/auth.roles.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const Roles = (roles: string[]) => SetMetadata('roles', roles);
4 |
--------------------------------------------------------------------------------
/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | export { AuthGuard } from './auth.guard';
2 | export { Authenticate, authenticate } from './auth.middleware';
3 | export { Roles } from './auth.roles.decorator';
4 |
--------------------------------------------------------------------------------
/src/common.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 | // tslint:disable-next-line: no-implicit-dependencies
3 | import { MongoMemoryServer } from 'mongodb-memory-server';
4 | import { User } from './users';
5 | import { Role } from './roles';
6 | import { UserRoleRelation } from './user-role-rel';
7 |
8 | export type ObjectId = mongoose.Schema.Types.ObjectId;
9 | export const ObjectID = mongoose.mongo.ObjectId;
10 |
11 | // testing
12 | const mongoMemoryServer = new MongoMemoryServer();
13 |
14 | export async function establishDbConnection() {
15 | const options = {
16 | useNewUrlParser: true,
17 | useUnifiedTopology: true,
18 | useCreateIndex: true,
19 | useFindAndModify: false,
20 | };
21 |
22 | const uri = await mongoMemoryServer.getUri();
23 | await mongoose.connect(uri, options);
24 | }
25 |
26 | export async function closeDbConnection() {
27 | await mongoose.disconnect();
28 | await mongoMemoryServer.stop();
29 | }
30 |
31 | // lodash sample
32 | export function sample(list: Array): UserRoleRelation;
33 | export function sample(list: Array): Role;
34 | export function sample(list: Array): User;
35 | export function sample(list: Array) {
36 | return list[Math.floor(Math.random() * list.length)];
37 | }
38 |
--------------------------------------------------------------------------------
/src/config/config.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigService } from './config.service';
3 |
4 | @Module({
5 | providers: [
6 | {
7 | provide: ConfigService,
8 | useValue: new ConfigService(`${process.env.NODE_ENV || 'dev'}.env`),
9 | },
10 | ],
11 | exports: [ConfigService],
12 | })
13 | export class ConfigModule {}
14 |
--------------------------------------------------------------------------------
/src/config/config.service.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from 'dotenv';
2 | import * as Joi from '@hapi/joi';
3 | import * as fs from 'fs';
4 |
5 | type EnvConfig = {
6 | [key: string]: string;
7 | };
8 |
9 | export class ConfigService {
10 | private readonly envConfig: EnvConfig;
11 |
12 | constructor(filePath: string) {
13 | const config = dotenv.parse(fs.readFileSync(filePath));
14 | this.envConfig = this.validateInput(config);
15 | }
16 |
17 | private validateInput(envConfig: EnvConfig): EnvConfig {
18 | const envVarSchema: Joi.ObjectSchema = Joi.object({
19 | NODE_ENV: Joi.string().valid('dev', 'prod', 'test'),
20 | // .default('dev'),
21 | MONGO_URL: Joi.string(),
22 | PORT: Joi.number(),
23 | URL: Joi.string(),
24 | });
25 |
26 | const { error, value: validatedEnvConfig } = envVarSchema.validate(envConfig);
27 |
28 | if (error) {
29 | throw new Error(`Config validation error: ${error.message}`);
30 | }
31 |
32 | return validatedEnvConfig;
33 | }
34 |
35 | get(key: string): string {
36 | return this.envConfig[key];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export { ConfigModule } from './config.module';
2 | export { ConfigService } from './config.service';
3 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
2 |
3 | export const ROLE_MODEL = 'ROLE_MODEL';
4 | export const USER_ROLE_RELATION_MODEL = 'USER_ROLE_RELATION_MODEL';
5 | export const USER_MODEL = 'USER_MODEL';
6 |
7 | export const ROLES_SERVICE = 'ROLES_SERVICE';
8 | export const USER_ROLE_RELATION_SERVICE = 'USER_ROLE_RELATION_SERVICE';
9 | export const USERS_SERVICE = 'USERS_SERVICE';
10 |
11 | export const AUTHENTICATE = 'AUTHENTICATE';
12 |
--------------------------------------------------------------------------------
/src/database/database.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { databaseProviders } from './database.providers';
3 | import { ConfigModule } from '../config';
4 |
5 | @Module({
6 | imports: [ConfigModule],
7 | providers: [...databaseProviders],
8 | exports: [...databaseProviders],
9 | })
10 | export class DatabaseModule {}
11 |
--------------------------------------------------------------------------------
/src/database/database.providers.ts:
--------------------------------------------------------------------------------
1 | import * as mongoose from 'mongoose';
2 | import { DATABASE_CONNECTION } from '../constants';
3 | import { ConfigService } from '../config';
4 |
5 | export const databaseProviders = [
6 | {
7 | provide: DATABASE_CONNECTION,
8 | useFactory: (config: ConfigService): Promise => {
9 | const MONGO_URL = config.get('MONGO_URL');
10 | const options = {
11 | useNewUrlParser: true,
12 | useUnifiedTopology: true,
13 | useCreateIndex: true,
14 | useFindAndModify: false,
15 | };
16 |
17 | return mongoose.connect(MONGO_URL, options);
18 | },
19 | inject: [ConfigService],
20 | },
21 | ];
22 |
--------------------------------------------------------------------------------
/src/database/index.ts:
--------------------------------------------------------------------------------
1 | export { DatabaseModule } from './database.module';
2 |
--------------------------------------------------------------------------------
/src/logging.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NestInterceptor,
4 | ExecutionContext,
5 | CallHandler,
6 | InternalServerErrorException,
7 | HttpException,
8 | HttpStatus,
9 | } from '@nestjs/common';
10 | import { Logger } from 'winston';
11 | import { Request } from 'express';
12 | import { Observable } from 'rxjs';
13 | import { catchError } from 'rxjs/operators';
14 |
15 | @Injectable()
16 | export class LoggingInterceptor implements NestInterceptor {
17 | constructor(private readonly logger: Logger) {}
18 |
19 | intercept(context: ExecutionContext, next: CallHandler): Observable {
20 | const request: Request = context.switchToHttp().getRequest();
21 | const { url, method } = request;
22 |
23 | return next.handle().pipe(
24 | catchError(error => {
25 | if (error instanceof HttpException) {
26 | throw error;
27 | }
28 |
29 | this.logger.error({
30 | url,
31 | method,
32 | message: error.message,
33 | stack: error.stack,
34 | });
35 |
36 | throw new InternalServerErrorException({
37 | statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
38 | message: 'Internal Server Error',
39 | });
40 | }),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ValidationPipe } from '@nestjs/common';
2 | import { NestFactory, Reflector } from '@nestjs/core';
3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4 | import { createLogger, format, Logger, transports } from 'winston';
5 | import { AppModule } from './app.module';
6 | import { authenticate as _authenticate, AuthGuard } from './auth';
7 | import { LoggingInterceptor } from './logging.interceptor';
8 | import { RolesMapper, rolesModel, RolesRepository, RolesService } from './roles';
9 | import { UserRoleRelationMapper, userRoleRelModel, UserRoleRelService } from './user-role-rel';
10 | import { UsersMapper, usersModel, UsersRepository, UsersService } from './users';
11 |
12 | export { bootstrap };
13 |
14 | async function bootstrap() {
15 | const mapper = new UserRoleRelationMapper();
16 | const userRoleRelService = new UserRoleRelService(userRoleRelModel, mapper);
17 |
18 | const rolesMapper = new RolesMapper();
19 | const rolesRepository = new RolesRepository(rolesModel, rolesMapper);
20 | const rolesService = new RolesService(rolesRepository);
21 |
22 | const usersMapper = new UsersMapper();
23 | const usersRepository = new UsersRepository(usersModel, usersMapper);
24 | const usersService = new UsersService(usersRepository, rolesService, userRoleRelService);
25 |
26 | const authenticate = _authenticate(usersService);
27 |
28 | const app = await NestFactory.create(
29 | AppModule.forRoot({ usersService, rolesService, userRoleRelService, authenticate }),
30 | );
31 |
32 | const logger: Logger = createLogger({
33 | level: 'info',
34 | format: format.json(),
35 | defaultMeta: { service: 'app-module' },
36 | transports: [new transports.Console()],
37 | });
38 |
39 | const reflector = new Reflector();
40 |
41 | app.useGlobalPipes(new ValidationPipe());
42 | app.useGlobalGuards(new AuthGuard(reflector));
43 | app.useGlobalInterceptors(new LoggingInterceptor(logger));
44 |
45 | const options = new DocumentBuilder()
46 | .setTitle('Nest.js example app')
47 | .setDescription('Use auto generated admin user credentials to authenticate into swagger')
48 | .setVersion('1.0')
49 | .addBasicAuth()
50 | .build();
51 |
52 | app.setGlobalPrefix('/api');
53 |
54 | const document = SwaggerModule.createDocument(app, options);
55 | SwaggerModule.setup('api', app, document);
56 |
57 | const port = process.env.PORT || 3000;
58 |
59 | await app.listen(port);
60 |
61 | console.log(`Listening on port: ${port}`);
62 | console.log(`Explore api on http://localhost:${port}/api`);
63 |
64 | return app;
65 | }
66 |
67 | if (require.main === module) {
68 | bootstrap();
69 | }
70 |
--------------------------------------------------------------------------------
/src/object-id.validation.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class ObjectIdValidationPipe implements PipeTransform {
5 | transform(value: any) {
6 | /**
7 | * mongoose validation does not provide proper validation of objectId
8 | * ex: "South Africa" or "zzzzzzzzzzzz" or any 12 character string is considered as valid id
9 | * issue: https://github.com/Automattic/mongoose/issues/1959
10 | */
11 | const objectIdRegEx = new RegExp('^[0-9a-fA-F]{24}$');
12 |
13 | if (!objectIdRegEx.test(value)) {
14 | throw new BadRequestException('ObjectId cast error. Invalid id');
15 | }
16 |
17 | return value;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/roles/__test__/mock.data.ts:
--------------------------------------------------------------------------------
1 | import { Role, RoleForCreate } from '../models/role.model';
2 | import { RolesMapper } from '../roles.repository';
3 | import { rolesModel } from '../schema/role.schema';
4 | import * as faker from 'faker';
5 |
6 | faker.seed(111);
7 |
8 | const mapper = new RolesMapper();
9 |
10 | export async function getMockRoles() {
11 | const roles: Role[] = [];
12 |
13 | for (let index = 1; index < 10; index++) {
14 | const role: RoleForCreate = {
15 | name: `role_${index}`,
16 | displayName: faker.lorem.word().toUpperCase(),
17 | description: faker.lorem.sentence(),
18 | };
19 |
20 | const mockRole = await rolesModel.create(role);
21 | roles.push(mapper.fromEntity(mockRole));
22 | }
23 |
24 | return roles;
25 | }
26 |
--------------------------------------------------------------------------------
/src/roles/__test__/roles.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { RolesService } from '../roles.service';
2 | import { establishDbConnection, closeDbConnection, sample, ObjectID } from '../../common';
3 | import { Role, RoleForCreate, RoleForUpdate } from '../models/role.model';
4 | import { getMockRoles } from './mock.data';
5 | import { RolesRepository, RolesMapper } from '../roles.repository';
6 | import { rolesModel } from '../schema/role.schema';
7 | import * as faker from 'faker';
8 | import { RoleAlreadyExistsError, RoleNotFoundError } from '../errors/errors';
9 |
10 | faker.seed(478);
11 |
12 | describe('Roles service', () => {
13 | let rolesService: RolesService;
14 | let mockRoles: Role[];
15 |
16 | beforeAll(async () => {
17 | await establishDbConnection();
18 | mockRoles = await getMockRoles();
19 | });
20 |
21 | afterAll(async () => {
22 | await closeDbConnection();
23 | });
24 |
25 | beforeEach(() => {
26 | const mapper = new RolesMapper();
27 | const repository = new RolesRepository(rolesModel, mapper);
28 |
29 | rolesService = new RolesService(repository);
30 | });
31 |
32 | describe('create', () => {
33 | it('should create role', async () => {
34 | const mockRole: RoleForCreate = {
35 | name: faker.lorem.word(),
36 | displayName: faker.lorem.word().toUpperCase(),
37 | description: faker.lorem.sentence(),
38 | };
39 |
40 | const result = await rolesService.create(mockRole);
41 | const role = result.right();
42 |
43 | expect(role.name).toBe(mockRole.name);
44 | expect(role.displayName).toBe(mockRole.displayName);
45 | expect(role.description).toBe(mockRole.description);
46 | });
47 |
48 | it('should return "RoleAlreadyExistsError" in case if same role exists', async () => {
49 | const role = sample(mockRoles);
50 |
51 | const result = await rolesService.create(role);
52 | const error = result.left();
53 |
54 | expect(error).toBeInstanceOf(RoleAlreadyExistsError);
55 | });
56 | });
57 |
58 | describe('get', () => {
59 | it('should get all roles', async () => {
60 | const roles = await rolesService.getAll();
61 |
62 | expect(roles.length).toBeGreaterThan(0);
63 | });
64 |
65 | it('should get specific role by id', async () => {
66 | const id = sample(mockRoles).id;
67 |
68 | const result = await rolesService.findOne(id);
69 | const role = result.right();
70 |
71 | expect(role.id).toBe(id);
72 | });
73 |
74 | it('should return "RoleNotFoundError" in case if role is not found', async () => {
75 | const id = new ObjectID().toHexString();
76 |
77 | const result = await rolesService.findOne(id);
78 | const error = result.left();
79 |
80 | expect(error).toBeInstanceOf(RoleNotFoundError);
81 | });
82 | });
83 |
84 | describe('update', () => {
85 | it('should update specific role by id', async () => {
86 | const id = sample(mockRoles).id;
87 | const patch: RoleForUpdate = {
88 | name: 'role_for_update',
89 | displayName: faker.lorem.word().toUpperCase(),
90 | description: faker.lorem.text(),
91 | };
92 |
93 | const result = await rolesService.update(id, patch);
94 | const updatedRole = result.right();
95 |
96 | expect(updatedRole.name).toBe(patch.name);
97 | expect(updatedRole.displayName).toBe(patch.displayName);
98 | expect(updatedRole.description).toBe(patch.description);
99 | });
100 |
101 | it('should return "RoleNotFoundError" in case if role for update is not exist', async () => {
102 | const id = new ObjectID().toHexString();
103 | const patch: RoleForUpdate = {
104 | description: 'some_text',
105 | };
106 |
107 | const result = await rolesService.update(id, patch);
108 | const error = result.left();
109 |
110 | expect(error).toBeInstanceOf(RoleNotFoundError);
111 | });
112 | });
113 |
114 | describe('delete', () => {
115 | it('should remove specific role by id', async () => {
116 | const id = sample(mockRoles).id;
117 |
118 | const result = await rolesService.delete(id);
119 | const role = result.right();
120 |
121 | expect(role.isDeleted).toBe(true);
122 | });
123 |
124 | it('should return "RoleNotFoundError" in case if role for delete is not exist', async () => {
125 | const id = new ObjectID().toHexString();
126 |
127 | const result = await rolesService.delete(id);
128 | const error = result.left();
129 |
130 | expect(error).toBeInstanceOf(RoleNotFoundError);
131 | });
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/src/roles/dto/role.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3 |
4 | export class CreateRoleDto {
5 | @IsString()
6 | @IsNotEmpty()
7 | @ApiProperty()
8 | readonly name: string;
9 |
10 | @IsString()
11 | @IsNotEmpty()
12 | @ApiProperty()
13 | readonly displayName: string;
14 |
15 | @IsString()
16 | @IsNotEmpty()
17 | @ApiProperty()
18 | readonly description: string;
19 | }
20 |
21 | export class UpdateRoleDto {
22 | @IsString()
23 | @IsOptional()
24 | @ApiPropertyOptional()
25 | readonly name?: string;
26 |
27 | @IsString()
28 | @IsOptional()
29 | @ApiPropertyOptional()
30 | readonly displayName?: string;
31 |
32 | @IsString()
33 | @IsOptional()
34 | @ApiPropertyOptional()
35 | readonly description?: string;
36 | }
37 |
38 | export class RolePresentationDto {
39 | @ApiProperty()
40 | id: string;
41 |
42 | @ApiProperty()
43 | name: string;
44 |
45 | @ApiProperty()
46 | displayName: string;
47 |
48 | @ApiProperty()
49 | description: string;
50 |
51 | @ApiProperty()
52 | isDeleted: boolean;
53 | }
54 |
--------------------------------------------------------------------------------
/src/roles/errors/errors.ts:
--------------------------------------------------------------------------------
1 | export class RoleNotFoundError extends Error {
2 | constructor(id: string) {
3 | super(`Role id:${id} not found`);
4 |
5 | this.name = 'RoleNotFoundError';
6 | }
7 | }
8 |
9 | export class RoleAlreadyExistsError extends Error {
10 | constructor(name: string) {
11 | super(`Role name:${name} already exists`);
12 |
13 | this.name = 'RoleAlreadyExistsError';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/roles/index.ts:
--------------------------------------------------------------------------------
1 | export * from './models/role.model';
2 | export * from './errors/errors';
3 | export * from './schema/role.schema';
4 | export * from './interfaces/interfaces';
5 | export { RolesMapper, RolesRepository } from './roles.repository';
6 | export { RolesService } from './roles.service';
7 | export { RolesModule } from './roles.module';
8 |
--------------------------------------------------------------------------------
/src/roles/interfaces/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { Role, RoleForCreate, RoleForUpdate } from '../models/role.model';
2 | import { RoleNotFoundError, RoleAlreadyExistsError } from '../errors/errors';
3 | import { Either } from 'monet';
4 |
5 | export interface IRolesRepository {
6 | create(role: RoleForCreate): Promise;
7 |
8 | getAll(): Promise;
9 |
10 | findOne(id: string): Promise>;
11 |
12 | isRoleAlreadyExists(name: string): Promise;
13 |
14 | update(id: string, patch: RoleForUpdate): Promise>;
15 |
16 | delete(id: string): Promise>;
17 | }
18 |
19 | export interface IRolesMapper {
20 | fromEntity(entity: any): Role;
21 | }
22 |
23 | export interface IRolesService {
24 | create(role: RoleForCreate): Promise>;
25 |
26 | getAll(): Promise;
27 |
28 | findOne(id: string): Promise>;
29 |
30 | update(id: string, patch: RoleForUpdate): Promise>;
31 |
32 | delete(id: string): Promise>;
33 | }
34 |
--------------------------------------------------------------------------------
/src/roles/models/role.model.ts:
--------------------------------------------------------------------------------
1 | export type Role = {
2 | readonly id: string;
3 | name: string;
4 | displayName: string;
5 | description: string;
6 | isDeleted: boolean;
7 | };
8 |
9 | export type RoleForCreate = Pick;
10 |
11 | export type RoleForUpdate = {
12 | name?: string;
13 | displayName?: string;
14 | description?: string;
15 | };
16 |
--------------------------------------------------------------------------------
/src/roles/roles.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | Post,
6 | Body,
7 | Patch,
8 | Delete,
9 | Inject,
10 | HttpStatus,
11 | NotFoundException,
12 | ConflictException,
13 | } from '@nestjs/common';
14 | import { CreateRoleDto, UpdateRoleDto, RolePresentationDto } from './dto/role.dto';
15 | import {
16 | ApiTags,
17 | ApiResponse,
18 | ApiBasicAuth,
19 | ApiOperation,
20 | ApiParam,
21 | ApiForbiddenResponse,
22 | ApiNotFoundResponse,
23 | ApiConflictResponse,
24 | } from '@nestjs/swagger';
25 | import { Roles } from '../auth';
26 | import { IRolesService } from './interfaces/interfaces';
27 | import { ROLES_SERVICE } from '../constants';
28 | import { identity } from 'rxjs';
29 | import { ObjectIdValidationPipe } from '../object-id.validation.pipe';
30 |
31 | @ApiBasicAuth()
32 | @ApiTags('roles')
33 | @Controller('roles')
34 | export class RolesController {
35 | constructor(
36 | @Inject(ROLES_SERVICE)
37 | private readonly rolesService: IRolesService,
38 | ) {}
39 |
40 | @Get()
41 | @Roles(['admin', 'guest'])
42 | @ApiResponse({ status: 200, type: [RolePresentationDto] })
43 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
44 | @ApiOperation({
45 | summary: 'Get roles',
46 | description: 'Get all existing roles',
47 | })
48 | async getAll(): Promise {
49 | return this.rolesService.getAll();
50 | }
51 |
52 | @Get(':id')
53 | @Roles(['admin', 'guest'])
54 | @ApiResponse({ status: 200, type: RolePresentationDto })
55 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
56 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
57 | @ApiParam({
58 | name: 'id',
59 | type: String,
60 | example: 'id: 5e5eb0418aa9340f913008e5',
61 | })
62 | @ApiOperation({
63 | summary: 'Get role',
64 | description: 'Get specific role by id',
65 | })
66 | async findOne(@Param('id', ObjectIdValidationPipe) id: string): Promise {
67 | const result = await this.rolesService.findOne(id);
68 |
69 | return result.cata(error => {
70 | throw new NotFoundException({
71 | status: HttpStatus.NOT_FOUND,
72 | error: error.name,
73 | message: error.message,
74 | });
75 | }, identity);
76 | }
77 |
78 | @Post()
79 | @Roles(['admin'])
80 | @ApiResponse({ status: 201, type: RolePresentationDto })
81 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
82 | @ApiConflictResponse({ status: 409, description: 'Role is already exists' })
83 | @ApiOperation({
84 | summary: 'Add new role',
85 | description: 'Create new role',
86 | })
87 | async createRole(@Body() createRoleDto: CreateRoleDto): Promise {
88 | const result = await this.rolesService.create(createRoleDto);
89 |
90 | return result.cata(error => {
91 | throw new ConflictException({
92 | status: HttpStatus.CONFLICT,
93 | error: error.name,
94 | message: error.message,
95 | });
96 | }, identity);
97 | }
98 |
99 | @Patch(':id')
100 | @Roles(['admin'])
101 | @ApiResponse({ status: 200, type: RolePresentationDto })
102 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
103 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
104 | @ApiParam({
105 | name: 'id',
106 | type: String,
107 | example: 'id: 5e5eb0418aa9340f913008e5',
108 | })
109 | @ApiOperation({
110 | summary: 'Update role',
111 | description: 'Update specific role by id',
112 | })
113 | async updateRole(
114 | @Param('id', ObjectIdValidationPipe) id: string,
115 | @Body() updateRoleDto: UpdateRoleDto,
116 | ): Promise {
117 | const result = await this.rolesService.update(id, updateRoleDto);
118 |
119 | return result.cata(error => {
120 | throw new NotFoundException({
121 | status: HttpStatus.NOT_FOUND,
122 | error: error.name,
123 | message: error.message,
124 | });
125 | }, identity);
126 | }
127 |
128 | @Delete(':id')
129 | @Roles(['admin'])
130 | @ApiResponse({ status: 200, type: RolePresentationDto })
131 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
132 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
133 | @ApiParam({
134 | name: 'id',
135 | type: String,
136 | example: 'id: 5e5eb0418aa9340f913008e5',
137 | })
138 | @ApiOperation({
139 | summary: 'Delete role',
140 | description: 'Remove specific role by id',
141 | })
142 | async removeRole(@Param('id', ObjectIdValidationPipe) id: string): Promise {
143 | const result = await this.rolesService.delete(id);
144 |
145 | return result.cata(error => {
146 | throw new NotFoundException({
147 | status: HttpStatus.NOT_FOUND,
148 | error: error.name,
149 | message: error.message,
150 | });
151 | }, identity);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/roles/roles.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, DynamicModule, NestModule, MiddlewareConsumer, Inject } from '@nestjs/common';
2 | import { RolesController } from './roles.controller';
3 | import { IRolesService } from './interfaces/interfaces';
4 | import { ROLES_SERVICE, AUTHENTICATE } from '../constants';
5 | import { Authenticate } from '../auth';
6 |
7 | @Module({})
8 | export class RolesModule implements NestModule {
9 | static forRoot(dependencies: {
10 | rolesService: IRolesService;
11 | authenticate: Authenticate;
12 | }): DynamicModule {
13 | return {
14 | module: RolesModule,
15 | controllers: [RolesController],
16 | providers: [
17 | {
18 | provide: ROLES_SERVICE,
19 | useValue: dependencies.rolesService,
20 | },
21 | {
22 | provide: AUTHENTICATE,
23 | useValue: dependencies.authenticate,
24 | },
25 | ],
26 | };
27 | }
28 |
29 | constructor(@Inject('AUTHENTICATE') private readonly authenticate: Authenticate) {}
30 |
31 | configure(consumer: MiddlewareConsumer) {
32 | consumer.apply(this.authenticate).forRoutes(RolesController);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/roles/roles.repository.ts:
--------------------------------------------------------------------------------
1 | import { Role, RoleForCreate, RoleForUpdate } from './models/role.model';
2 | import { Model } from 'mongoose';
3 | import { RoleDocument } from './schema/role.schema';
4 | import { IRolesMapper, IRolesRepository } from './interfaces/interfaces';
5 | import { RoleNotFoundError } from './errors/errors';
6 | import { Either, Left, Right } from 'monet';
7 |
8 | export class RolesMapper implements IRolesMapper {
9 | fromEntity(entity: any): Role {
10 | return {
11 | id: entity._id.toString(),
12 | name: entity.name,
13 | description: entity.description,
14 | displayName: entity.displayName,
15 | isDeleted: entity.isDeleted,
16 | };
17 | }
18 | }
19 |
20 | export class RolesRepository implements IRolesRepository {
21 | constructor(
22 | private readonly database: Model,
23 | private readonly mapper: IRolesMapper,
24 | ) {}
25 |
26 | async create(role: RoleForCreate): Promise {
27 | const createdRole = await this.database.create(role);
28 | return this.mapper.fromEntity(createdRole);
29 | }
30 |
31 | async getAll(): Promise {
32 | const roles = await this.database.find();
33 | return roles.map(this.mapper.fromEntity);
34 | }
35 |
36 | async findOne(id: string): Promise> {
37 | const role = await this.database.findById(id);
38 |
39 | if (!role) {
40 | return Left(new RoleNotFoundError(id));
41 | }
42 | return Right(this.mapper.fromEntity(role));
43 | }
44 |
45 | async isRoleAlreadyExists(name: string): Promise {
46 | const [role] = await this.database.find({ name });
47 | return role ? true : false;
48 | }
49 |
50 | async update(id: string, patch: RoleForUpdate): Promise> {
51 | const role = await this.findOne(id);
52 |
53 | if (!role.isRight()) {
54 | return Left(new RoleNotFoundError(id));
55 | }
56 |
57 | const updated = await this.database.findByIdAndUpdate(id, patch, {
58 | new: true,
59 | });
60 | return Right(this.mapper.fromEntity(updated));
61 | }
62 |
63 | async delete(id: string): Promise> {
64 | const role = await this.findOne(id);
65 |
66 | if (!role.isRight()) {
67 | return Left(new RoleNotFoundError(id));
68 | }
69 |
70 | const deleted = await this.database.findByIdAndUpdate(id, { isDeleted: true }, { new: true });
71 | return Right(this.mapper.fromEntity(deleted));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/roles/roles.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { RoleForCreate, Role, RoleForUpdate } from './models/role.model';
3 | import { IRolesRepository, IRolesService } from './interfaces/interfaces';
4 | import { RoleNotFoundError, RoleAlreadyExistsError } from './errors/errors';
5 | import { Either, Left, Right } from 'monet';
6 |
7 | @Injectable()
8 | export class RolesService implements IRolesService {
9 | constructor(private readonly rolesRepo: IRolesRepository) {}
10 |
11 | async create(role: RoleForCreate): Promise> {
12 | const isRoleExists = await this.rolesRepo.isRoleAlreadyExists(role.name);
13 |
14 | if (isRoleExists) {
15 | return Left(new RoleAlreadyExistsError(role.name));
16 | }
17 |
18 | const createdRole = await this.rolesRepo.create(role);
19 | return Right(createdRole);
20 | }
21 |
22 | async getAll(): Promise {
23 | return this.rolesRepo.getAll();
24 | }
25 |
26 | async findOne(id: string): Promise> {
27 | return this.rolesRepo.findOne(id);
28 | }
29 |
30 | async update(id: string, patch: RoleForUpdate): Promise> {
31 | return this.rolesRepo.update(id, patch);
32 | }
33 |
34 | async delete(id: string): Promise> {
35 | return this.rolesRepo.delete(id);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/roles/schema/role.schema.ts:
--------------------------------------------------------------------------------
1 | import { Schema, Document, connection } from 'mongoose';
2 | import { ObjectId } from '../../common';
3 | import { ROLE_MODEL } from '../../constants';
4 |
5 | export interface RoleDocument extends Document {
6 | readonly _id: ObjectId;
7 | readonly name: string;
8 | readonly displayName: string;
9 | readonly description: string;
10 | readonly isDeleted: boolean;
11 | }
12 |
13 | export const RoleSchema = new Schema({
14 | name: { type: String, required: true, unique: true },
15 | displayName: { type: String, required: true },
16 | description: { type: String, required: true },
17 | isDeleted: { type: Boolean, default: false },
18 | });
19 |
20 | export const rolesModel = connection.model(ROLE_MODEL, RoleSchema, 'identity-roles');
21 |
--------------------------------------------------------------------------------
/src/user-role-rel/__test__/mock.data.ts:
--------------------------------------------------------------------------------
1 | // import { UserRoleRelationForCreate } from '../models/user-role-rel.model';
2 | import { ObjectID } from '../../common';
3 | import { userRoleRelModel } from '../schema/user-role-rel.schema';
4 | import { UserRoleRelationMapper } from '../user-role-rel.service';
5 |
6 | const mapper = new UserRoleRelationMapper();
7 |
8 | export async function getMockUserRoleRelations() {
9 | const relations = [];
10 |
11 | for (let index = 0; index < 20; index++) {
12 | const rel = {
13 | roleId: new ObjectID(),
14 | userId: new ObjectID(),
15 | };
16 |
17 | const mockRelation = await userRoleRelModel.create(rel);
18 |
19 | relations.push(mapper.fromEntity(mockRelation));
20 | }
21 |
22 | return relations;
23 | }
24 |
--------------------------------------------------------------------------------
/src/user-role-rel/__test__/user-role-rel.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { UserRoleRelationMapper, UserRoleRelService } from '../user-role-rel.service';
2 | import { userRoleRelModel } from '../schema/user-role-rel.schema';
3 | import { getMockUserRoleRelations } from './mock.data';
4 | import { establishDbConnection, closeDbConnection, sample, ObjectID } from '../../common';
5 | import { UserRoleRelation, UserRoleRelationForCreate } from '../models/user-role-rel.model';
6 | import {
7 | RoleRelationNotFoundError,
8 | RelationNotFoundError,
9 | RoleRelationAlreadyExistsError,
10 | } from '../errors/errors';
11 |
12 | describe('UserRoleRelations service', () => {
13 | let userRoleRelService: UserRoleRelService;
14 | let mockRelations: UserRoleRelation[];
15 |
16 | beforeEach(() => {
17 | const mapper = new UserRoleRelationMapper();
18 | userRoleRelService = new UserRoleRelService(userRoleRelModel, mapper);
19 | });
20 |
21 | beforeAll(async () => {
22 | await establishDbConnection();
23 | mockRelations = await getMockUserRoleRelations();
24 | });
25 |
26 | afterAll(async () => {
27 | await closeDbConnection();
28 | });
29 |
30 | describe('get', () => {
31 | it('should get all relations', async () => {
32 | const relations = await userRoleRelService.getAll();
33 |
34 | expect(relations.length).toBeGreaterThan(0);
35 | });
36 |
37 | it('should get specific relation by userId', async () => {
38 | const userId = sample(mockRelations).userId;
39 |
40 | const result = await userRoleRelService.getByAccount(userId);
41 | const relations = result.right();
42 |
43 | expect(relations.length).toBeGreaterThan(0);
44 | relations.every(relation => {
45 | expect(relation.userId).toBe(userId);
46 | });
47 | });
48 |
49 | it('should return "RoleRelationNotFoundError" in case if relation not found', async () => {
50 | const userId = new ObjectID().toHexString();
51 |
52 | const result = await userRoleRelService.getByAccount(userId);
53 | const error = result.left();
54 |
55 | expect(error).toBeInstanceOf(RoleRelationNotFoundError);
56 | });
57 | });
58 |
59 | describe('create', () => {
60 | it('should create new relation', async () => {
61 | const rel: UserRoleRelationForCreate = {
62 | roleId: new ObjectID().toHexString(),
63 | userId: new ObjectID().toHexString(),
64 | };
65 |
66 | const result = await userRoleRelService.create(rel);
67 | const createdRel = result.right();
68 |
69 | expect(createdRel.roleId).toBe(rel.roleId);
70 | expect(createdRel.userId).toBe(rel.userId);
71 | });
72 |
73 | it('should return "RoleRelationAlreadyExistsError" if relation already exists', async () => {
74 | const existingRelation = sample(mockRelations);
75 |
76 | const result = await userRoleRelService.create(existingRelation);
77 | const error = result.left();
78 |
79 | expect(error).toBeInstanceOf(RoleRelationAlreadyExistsError);
80 | });
81 | });
82 |
83 | describe('delete', () => {
84 | it('should remove specific relation by id', async () => {
85 | const id = sample(mockRelations).id;
86 |
87 | const result = await userRoleRelService.delete(id);
88 | const removedRelation = result.right();
89 |
90 | expect(removedRelation.isDeleted).toBe(true);
91 | });
92 |
93 | it('should return "RelationNotFoundError" if relation for delete not found', async () => {
94 | const id = new ObjectID().toHexString();
95 |
96 | const result = await userRoleRelService.delete(id);
97 | const error = result.left();
98 |
99 | expect(error).toBeInstanceOf(RelationNotFoundError);
100 | });
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/src/user-role-rel/dto/user-role-rel.dto.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { IsString, IsNotEmpty } from 'class-validator';
3 |
4 | export class CreateRelationDto {
5 | @IsString()
6 | @IsNotEmpty()
7 | @ApiProperty()
8 | userId: string;
9 |
10 | @IsString()
11 | @IsNotEmpty()
12 | @ApiProperty()
13 | roleId: string;
14 | }
15 |
16 | export class UserRoleRelPresentationDto {
17 | @ApiProperty()
18 | id: string;
19 |
20 | @ApiProperty()
21 | userId: string;
22 |
23 | @ApiProperty()
24 | roleId: string;
25 |
26 | @ApiProperty()
27 | isDeleted: boolean;
28 | }
29 |
--------------------------------------------------------------------------------
/src/user-role-rel/errors/errors.ts:
--------------------------------------------------------------------------------
1 | export class RoleRelationNotFoundError extends Error {
2 | constructor(id: string) {
3 | super(`Role relation to user ${id} not found`);
4 | this.name = 'RoleRelationNotFoundError';
5 | }
6 | }
7 |
8 | export class RelationNotFoundError extends Error {
9 | constructor(id: string) {
10 | super(`Relation ${id} not found`);
11 | this.name = 'RelationNotFoundError';
12 | }
13 | }
14 |
15 | export class RoleRelationAlreadyExistsError extends Error {
16 | constructor(roleId: string) {
17 | super(`User role relation with role id:${roleId} already exists`);
18 | this.name = 'RoleRelationAlreadyExistsError';
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/user-role-rel/index.ts:
--------------------------------------------------------------------------------
1 | export * from './errors/errors';
2 | export * from './interfaces/interfaces';
3 | export * from './models/user-role-rel.model';
4 | export * from './schema/user-role-rel.schema';
5 | export { UserRoleRelController } from './user-role-rel.controller';
6 | export { UserRoleRelModule } from './user-role-rel.module';
7 | export { UserRoleRelService, UserRoleRelationMapper } from './user-role-rel.service';
8 |
--------------------------------------------------------------------------------
/src/user-role-rel/interfaces/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { UserRoleRelation, UserRoleRelationForCreate } from '../models/user-role-rel.model';
2 | import { Either } from 'monet';
3 | import {
4 | RoleRelationNotFoundError,
5 | RelationNotFoundError,
6 | RoleRelationAlreadyExistsError,
7 | } from '../errors/errors';
8 |
9 | export interface IUserRoleRelationMapper {
10 | fromEntity(entity: any): UserRoleRelation;
11 | }
12 |
13 | export interface IUserRoleRelService {
14 | getAll(): Promise;
15 |
16 | getByAccount(userId: string): Promise>;
17 |
18 | create(
19 | relation: UserRoleRelationForCreate,
20 | ): Promise>;
21 |
22 | delete(id: string): Promise>;
23 | }
24 |
--------------------------------------------------------------------------------
/src/user-role-rel/models/user-role-rel.model.ts:
--------------------------------------------------------------------------------
1 | export type UserRoleRelation = {
2 | readonly id: string;
3 | userId: string;
4 | roleId: string;
5 | isDeleted: boolean;
6 | };
7 |
8 | export type UserRoleRelationForCreate = Omit<
9 | UserRoleRelation,
10 | 'id' | 'isDeleted'
11 | >;
12 |
--------------------------------------------------------------------------------
/src/user-role-rel/schema/user-role-rel.schema.ts:
--------------------------------------------------------------------------------
1 | import { Schema, Document, connection } from 'mongoose';
2 | import { ObjectId } from '../../common';
3 | import { USER_ROLE_RELATION_MODEL } from '../../constants';
4 |
5 | export interface UserRoleRelDocument extends Document {
6 | readonly id: ObjectId;
7 | readonly userId: string;
8 | readonly roleId: string;
9 | }
10 |
11 | export const UserRoleRelSchema: Schema = new Schema({
12 | userId: { type: String, required: true },
13 | roleId: { type: String, required: true },
14 | isDeleted: { type: Boolean, default: false },
15 | });
16 |
17 | export const userRoleRelModel = connection.model(
18 | USER_ROLE_RELATION_MODEL,
19 | UserRoleRelSchema,
20 | 'identity-role-relations',
21 | );
22 |
--------------------------------------------------------------------------------
/src/user-role-rel/user-role-rel.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Param,
5 | Delete,
6 | Inject,
7 | NotFoundException,
8 | HttpStatus,
9 | Post,
10 | Body,
11 | ConflictException,
12 | Query,
13 | } from '@nestjs/common';
14 | import {
15 | ApiTags,
16 | ApiResponse,
17 | ApiBasicAuth,
18 | ApiForbiddenResponse,
19 | ApiNotFoundResponse,
20 | ApiOperation,
21 | ApiParam,
22 | ApiConflictResponse,
23 | ApiQuery,
24 | } from '@nestjs/swagger';
25 | import { UserRoleRelPresentationDto, CreateRelationDto } from './dto/user-role-rel.dto';
26 | import { Roles } from '../auth';
27 | import { IUserRoleRelService } from './interfaces/interfaces';
28 | import { USER_ROLE_RELATION_SERVICE } from '../constants';
29 | import { identity } from 'rxjs';
30 | import { ObjectIdValidationPipe } from '../object-id.validation.pipe';
31 |
32 | @ApiBasicAuth()
33 | @ApiTags('relations')
34 | @Controller('relations')
35 | export class UserRoleRelController {
36 | constructor(
37 | @Inject(USER_ROLE_RELATION_SERVICE)
38 | private readonly userRoleRelService: IUserRoleRelService,
39 | ) {}
40 |
41 | @Get()
42 | @Roles(['admin', 'guest'])
43 | @ApiResponse({ status: 200, type: [UserRoleRelPresentationDto] })
44 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
45 | @ApiOperation({
46 | summary: 'Get relations',
47 | description: 'Get all existing relations',
48 | })
49 | async getAll(): Promise {
50 | return this.userRoleRelService.getAll();
51 | }
52 |
53 | @Get('user')
54 | @Roles(['admin', 'guest'])
55 | @ApiResponse({ status: 200, type: UserRoleRelPresentationDto })
56 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
57 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
58 | @ApiQuery({
59 | name: 'userId',
60 | required: true,
61 | example: 'userId: 5e5eb0418aa9340f913008e5',
62 | type: String,
63 | })
64 | @ApiOperation({
65 | summary: 'Get relation',
66 | description: 'Get specific relation by userId',
67 | })
68 | async getUserRoleRelByUserId(
69 | @Query('userId', ObjectIdValidationPipe) userId: string,
70 | ): Promise {
71 | const result = await this.userRoleRelService.getByAccount(userId);
72 |
73 | return result.cata(error => {
74 | throw new NotFoundException({
75 | status: HttpStatus.NOT_FOUND,
76 | error: error.name,
77 | message: error.message,
78 | });
79 | }, identity);
80 | }
81 |
82 | @Post()
83 | @Roles(['admin'])
84 | @ApiResponse({ status: 201, type: UserRoleRelPresentationDto })
85 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
86 | @ApiConflictResponse({ status: 409, description: 'Relation is already exists' })
87 | @ApiOperation({
88 | summary: 'Assign role',
89 | description: 'Assign role to specific user',
90 | })
91 | async create(@Body() createRelationDto: CreateRelationDto) {
92 | const result = await this.userRoleRelService.create(createRelationDto);
93 | return result.cata(error => {
94 | throw new ConflictException({
95 | status: HttpStatus.CONFLICT,
96 | error: error.name,
97 | message: error.message,
98 | });
99 | }, identity);
100 | }
101 |
102 | @Delete(':id')
103 | @Roles(['admin'])
104 | @ApiResponse({ status: 200, type: UserRoleRelPresentationDto })
105 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
106 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
107 | @ApiParam({
108 | name: 'id',
109 | type: String,
110 | example: 'id: 5e5eb0418aa9340f913008e5',
111 | })
112 | @ApiOperation({
113 | summary: 'Remove relation',
114 | description: 'Remove specific relation by id',
115 | })
116 | async removeRel(
117 | @Param('id', ObjectIdValidationPipe) id: string,
118 | ): Promise {
119 | const result = await this.userRoleRelService.delete(id);
120 |
121 | return result.cata(error => {
122 | throw new NotFoundException({
123 | status: HttpStatus.NOT_FOUND,
124 | error: error.name,
125 | message: error.message,
126 | });
127 | }, identity);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/user-role-rel/user-role-rel.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, DynamicModule, NestModule, Inject, MiddlewareConsumer } from '@nestjs/common';
2 | import { UserRoleRelController } from './user-role-rel.controller';
3 | import { IUserRoleRelService } from './interfaces/interfaces';
4 | import { USER_ROLE_RELATION_SERVICE, AUTHENTICATE } from '../constants';
5 | import { Authenticate } from '../auth';
6 |
7 | @Module({})
8 | export class UserRoleRelModule implements NestModule {
9 | static forRoot(dependencies: {
10 | userRoleRelService: IUserRoleRelService;
11 | authenticate: Authenticate;
12 | }): DynamicModule {
13 | return {
14 | module: UserRoleRelModule,
15 | controllers: [UserRoleRelController],
16 | providers: [
17 | {
18 | provide: USER_ROLE_RELATION_SERVICE,
19 | useValue: dependencies.userRoleRelService,
20 | },
21 | {
22 | provide: AUTHENTICATE,
23 | useValue: dependencies.authenticate,
24 | },
25 | ],
26 | };
27 | }
28 |
29 | constructor(@Inject(AUTHENTICATE) private readonly authenticate: Authenticate) {}
30 |
31 | configure(consumer: MiddlewareConsumer) {
32 | consumer.apply(this.authenticate).forRoutes(UserRoleRelController);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/user-role-rel/user-role-rel.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { UserRoleRelation, UserRoleRelationForCreate } from './models/user-role-rel.model';
3 | import { Model } from 'mongoose';
4 | import { UserRoleRelDocument } from './schema/user-role-rel.schema';
5 | import { IUserRoleRelationMapper, IUserRoleRelService } from './interfaces/interfaces';
6 | import { Either, Left, Right } from 'monet';
7 | import {
8 | RoleRelationNotFoundError,
9 | RelationNotFoundError,
10 | RoleRelationAlreadyExistsError,
11 | } from './errors/errors';
12 |
13 | export class UserRoleRelationMapper implements IUserRoleRelationMapper {
14 | fromEntity(entity: any): UserRoleRelation {
15 | return {
16 | id: entity._id.toString(),
17 | roleId: entity.roleId,
18 | userId: entity.userId,
19 | isDeleted: entity.isDeleted,
20 | };
21 | }
22 | }
23 |
24 | @Injectable()
25 | export class UserRoleRelService implements IUserRoleRelService {
26 | constructor(
27 | private readonly relationModel: Model,
28 | private readonly mapper: IUserRoleRelationMapper,
29 | ) {}
30 |
31 | private async checkExistence(relation: UserRoleRelationForCreate): Promise {
32 | const relations = await this.relationModel.find({
33 | roleId: relation.roleId,
34 | userId: relation.userId,
35 | });
36 | return relations.length ? true : false;
37 | }
38 |
39 | async create(
40 | relation: UserRoleRelationForCreate,
41 | ): Promise> {
42 | const isRelationExists = await this.checkExistence(relation);
43 |
44 | if (isRelationExists) {
45 | return Left(new RoleRelationAlreadyExistsError(relation.roleId));
46 | }
47 |
48 | const created = await this.relationModel.create(relation);
49 | return Right(this.mapper.fromEntity(created));
50 | }
51 |
52 | async getAll(): Promise {
53 | const relations = await this.relationModel.find();
54 | return relations.map(this.mapper.fromEntity);
55 | }
56 |
57 | async getByAccount(id: string): Promise> {
58 | const relations = await this.relationModel.find({ userId: id });
59 |
60 | if (!relations.length) {
61 | return Left(new RoleRelationNotFoundError(id));
62 | }
63 | return Right(relations.map(this.mapper.fromEntity));
64 | }
65 |
66 | async delete(id: string): Promise> {
67 | const userRoleRel = await this.relationModel.findById(id);
68 |
69 | if (!userRoleRel) {
70 | return Left(new RelationNotFoundError(id));
71 | }
72 |
73 | const deleted = await this.relationModel.findByIdAndUpdate(
74 | id,
75 | { isDeleted: true },
76 | { new: true },
77 | );
78 |
79 | return Right(this.mapper.fromEntity(deleted));
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/users/__test__/mock.data.ts:
--------------------------------------------------------------------------------
1 | import { usersModel } from '../schema/user.schema';
2 | import { UserForCreate, User } from '../models/user.model';
3 | import { UsersMapper } from '../users.repository';
4 | import * as faker from 'faker';
5 | import { userRoleRelModel } from '../../user-role-rel/schema/user-role-rel.schema';
6 | import { RoleForCreate } from '../../roles/models/role.model';
7 | import { rolesModel } from '../../roles/schema/role.schema';
8 |
9 | faker.seed(678);
10 |
11 | const mapper = new UsersMapper();
12 |
13 | export async function getMockUsers() {
14 | const users: User[] = [];
15 |
16 | for (let index = 1; index < 20; index++) {
17 | const user: UserForCreate = {
18 | username: faker.internet.userName(),
19 | password: faker.internet.password(),
20 | };
21 |
22 | const role: RoleForCreate = {
23 | name: `role_${index}`,
24 | displayName: faker.lorem.word().toUpperCase(),
25 | description: faker.lorem.sentence(),
26 | };
27 |
28 | const mockRole = await rolesModel.create(role);
29 | const mockUser = await usersModel.create(user);
30 | users.push(mapper.fromEntity(mockUser));
31 |
32 | const relation = {
33 | userId: mockUser._id.toString(),
34 | roleId: mockRole._id.toString(),
35 | };
36 | await userRoleRelModel.create(relation);
37 | }
38 |
39 | return users;
40 | }
41 |
--------------------------------------------------------------------------------
/src/users/__test__/users.repository.spec.ts:
--------------------------------------------------------------------------------
1 | import { UsersRepository, UsersMapper } from '../users.repository';
2 | import { usersModel } from '../schema/user.schema';
3 | import { UserForCreate, UserForUpdate, User } from '../models/user.model';
4 | import { establishDbConnection, closeDbConnection, sample, ObjectID } from '../../common';
5 | import { getMockUsers } from './mock.data';
6 | import * as faker from 'faker';
7 | import { UserNotFoundError } from '../errors/errors';
8 |
9 | faker.seed(347);
10 |
11 | describe('Users repository', () => {
12 | const mapper = new UsersMapper();
13 |
14 | let repository: UsersRepository;
15 | let mockUsers: User[];
16 |
17 | beforeAll(async () => {
18 | await establishDbConnection();
19 | mockUsers = await getMockUsers();
20 | });
21 |
22 | afterAll(async () => {
23 | await closeDbConnection();
24 | });
25 |
26 | beforeEach(() => {
27 | repository = new UsersRepository(usersModel, mapper);
28 | });
29 |
30 | describe('create', () => {
31 | it('should create user', async () => {
32 | const user: UserForCreate = {
33 | username: faker.internet.userName(),
34 | password: faker.internet.password(),
35 | };
36 |
37 | const createdUser = await repository.create(user);
38 |
39 | expect(createdUser.id).toBeDefined();
40 | expect(createdUser.username).toBe(user.username);
41 | expect(createdUser.password).toBe(user.password);
42 | });
43 | });
44 |
45 | describe('get', () => {
46 | it('should get all users', async () => {
47 | const users = await repository.getAll();
48 |
49 | expect(users.length).toBeGreaterThan(0);
50 | });
51 |
52 | it('should get non deleted users', async () => {
53 | const users = await repository.getAll({ isDeleted: false });
54 |
55 | users.map(user => {
56 | expect(user.isDeleted).toBe(false);
57 | });
58 | });
59 |
60 | it('should get user by id', async () => {
61 | const userForCreate = {
62 | id: '5e4c3dbfc074d01fe5dac20f',
63 | username: faker.internet.userName(),
64 | password: faker.internet.password(),
65 | };
66 |
67 | const user = await repository.create(userForCreate);
68 | const result = await repository.findOne(user.id);
69 | const foundUser = result.right();
70 |
71 | expect(foundUser.id).toBe(user.id);
72 | });
73 |
74 | it('should return "UserNotFoundError" if user not found', async () => {
75 | const id = new ObjectID().toHexString();
76 |
77 | const result = await repository.findOne(id);
78 | const error = result.left();
79 |
80 | expect(error).toBeInstanceOf(UserNotFoundError);
81 | });
82 | });
83 |
84 | describe('update', () => {
85 | it('should update user', async () => {
86 | const id = sample(mockUsers).id;
87 | const patch: UserForUpdate = {
88 | username: faker.internet.userName(),
89 | };
90 |
91 | const result = await repository.update(id, patch);
92 | const updatedUser = result.right();
93 |
94 | expect(updatedUser.username).toBe(patch.username);
95 | });
96 |
97 | it('should return "UserNotFoundError" if user for update not found', async () => {
98 | const id = new ObjectID().toHexString();
99 | const patch: UserForUpdate = {
100 | username: faker.internet.userName(),
101 | };
102 |
103 | const result = await repository.update(id, patch);
104 | const error = result.left();
105 |
106 | expect(error).toBeInstanceOf(UserNotFoundError);
107 | });
108 | });
109 |
110 | describe('delete', () => {
111 | it('should remove user by id', async () => {
112 | const id = sample(mockUsers).id;
113 |
114 | const result = await repository.delete(id);
115 | const user = result.right();
116 |
117 | expect(user.isDeleted).toBe(true);
118 | });
119 |
120 | it('should return "UserNotFoundError" if user for delete not found', async () => {
121 | const id = new ObjectID().toHexString();
122 |
123 | const result = await repository.delete(id);
124 | const error = result.left();
125 |
126 | expect(error).toBeInstanceOf(UserNotFoundError);
127 | });
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/src/users/__test__/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { closeDbConnection, establishDbConnection, sample, ObjectID } from '../../common';
2 | import {
3 | RolesMapper,
4 | rolesModel,
5 | RolesRepository,
6 | RolesService,
7 | RoleNotFoundError,
8 | } from '../../roles';
9 | import {
10 | UserRoleRelationMapper,
11 | userRoleRelModel,
12 | UserRoleRelService,
13 | RoleRelationNotFoundError,
14 | } from '../../user-role-rel';
15 | import { User, UserForCreate } from '../models/user.model';
16 | import { usersModel } from '../schema/user.schema';
17 | import { UsersMapper, UsersRepository } from '../users.repository';
18 | import { UsersService } from '../users.service';
19 | import { getMockUsers } from './mock.data';
20 | import { UserNotFoundError, UserAlreadyExistsError } from '../errors/errors';
21 | import { Left } from 'monet';
22 |
23 | describe('Users service', () => {
24 | let usersService: UsersService;
25 | let userRoleRelService: UserRoleRelService;
26 | let rolesService: RolesService;
27 | let mockUsers: User[];
28 |
29 | beforeEach(() => {
30 | const rolesMapper = new RolesMapper();
31 | const rolesRepo = new RolesRepository(rolesModel, rolesMapper);
32 | rolesService = new RolesService(rolesRepo);
33 |
34 | const roleRelMapper = new UserRoleRelationMapper();
35 | userRoleRelService = new UserRoleRelService(userRoleRelModel, roleRelMapper);
36 |
37 | const usersMapper = new UsersMapper();
38 | const usersRepo = new UsersRepository(usersModel, usersMapper);
39 | usersService = new UsersService(usersRepo, rolesService, userRoleRelService);
40 | });
41 |
42 | beforeAll(async () => {
43 | await establishDbConnection();
44 | mockUsers = await getMockUsers();
45 | });
46 |
47 | afterAll(async () => {
48 | await closeDbConnection();
49 | });
50 |
51 | describe('On module init', () => {
52 | it('should add admin user on module init', async () => {
53 | await usersService.onModuleInit();
54 |
55 | const [result] = await usersService.getAll({ username: 'admin' });
56 |
57 | expect(result).toMatchObject({
58 | username: 'admin',
59 | password: '123',
60 | });
61 | });
62 | });
63 |
64 | describe('get', () => {
65 | it('should get all users', async () => {
66 | const users = await usersService.getAll();
67 |
68 | expect(users.length).toBeGreaterThan(0);
69 | });
70 |
71 | it('should get specific user by id', async () => {
72 | const id = sample(mockUsers).id;
73 | const result = await usersService.findOne(id);
74 | const user = result.right();
75 |
76 | expect(user.id).toBe(id);
77 | });
78 |
79 | it('should return "UserNotFoundError" if user not found', async () => {
80 | const id = new ObjectID().toHexString();
81 |
82 | const result = await usersService.findOne(id);
83 | const error = result.left();
84 |
85 | expect(error).toBeInstanceOf(UserNotFoundError);
86 | });
87 |
88 | it('should get user credentials', async () => {
89 | const { username, password } = sample(mockUsers);
90 |
91 | const result = await usersService.getUserCredentials(username, password);
92 | const credentials = result.right();
93 | const roles = credentials.roles;
94 |
95 | expect(credentials.username).toBe(username);
96 | expect(roles.length).toBeGreaterThan(0);
97 | });
98 |
99 | it('should return "RoleRelationNotFoundError" if relation to get credentials not found', async () => {
100 | const { username, password } = sample(mockUsers);
101 | const id = new ObjectID().toHexString();
102 |
103 | jest
104 | .spyOn(userRoleRelService, 'getByAccount')
105 | .mockImplementationOnce(async () => Left(new RoleRelationNotFoundError(id)));
106 |
107 | const result = await usersService.getUserCredentials(username, password);
108 | const error = result.left();
109 |
110 | expect(error).toBeInstanceOf(RoleRelationNotFoundError);
111 | });
112 |
113 | it('should return "RoleNotFoundError" if role to get credentials not found', async () => {
114 | const { username, password } = sample(mockUsers);
115 | const id = new ObjectID().toHexString();
116 |
117 | jest
118 | .spyOn(rolesService, 'findOne')
119 | .mockImplementationOnce(async () => Left(new RoleNotFoundError(id)));
120 |
121 | const result = await usersService.getUserCredentials(username, password);
122 | const error = result.left();
123 |
124 | expect(error).toBeInstanceOf(RoleNotFoundError);
125 | });
126 | });
127 |
128 | describe('create', () => {
129 | it('should create new user', async () => {
130 | const userForCreate: UserForCreate = {
131 | username: 'test_user',
132 | password: 'pwd',
133 | };
134 |
135 | const result = await usersService.addUser(userForCreate);
136 | const user = result.right();
137 |
138 | expect(user.username).toBe(userForCreate.username);
139 | expect(user.password).toBe(userForCreate.password);
140 | });
141 |
142 | it('should return "UserAlreadyExistsError" if user already exists', async () => {
143 | const existingUser = sample(mockUsers);
144 |
145 | const result = await usersService.addUser(existingUser);
146 | const error = result.left();
147 |
148 | expect(error).toBeInstanceOf(UserAlreadyExistsError);
149 | });
150 | });
151 |
152 | describe('update', () => {
153 | it('should update specific user by id', async () => {
154 | const id = sample(mockUsers).id;
155 | const patch = {
156 | username: 'updated_user',
157 | };
158 |
159 | const result = await usersService.updateUser(id, patch);
160 | const updatedUser = result.right();
161 |
162 | expect(updatedUser.id).toBe(id);
163 | expect(updatedUser.username).toBe(patch.username);
164 | });
165 |
166 | it('should return "UserNotFoundError" if user for update not found', async () => {
167 | const id = new ObjectID().toHexString();
168 | const patch = {
169 | username: 'updated_user',
170 | };
171 |
172 | const result = await usersService.updateUser(id, patch);
173 | const error = result.left();
174 |
175 | expect(error).toBeInstanceOf(UserNotFoundError);
176 | });
177 | });
178 |
179 | describe('delete', () => {
180 | it('should remove specific user by id', async () => {
181 | const id = sample(mockUsers).id;
182 |
183 | const result = await usersService.removeUser(id);
184 | const deletedUser = result.right();
185 |
186 | expect(deletedUser.isDeleted).toBe(true);
187 | });
188 |
189 | it('should return "UserNotFoundError" if user for delete not found', async () => {
190 | const id = new ObjectID().toHexString();
191 |
192 | const result = await usersService.removeUser(id);
193 | const error = result.left();
194 |
195 | expect(error).toBeInstanceOf(UserNotFoundError);
196 | });
197 | });
198 | });
199 |
--------------------------------------------------------------------------------
/src/users/dto/user.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3 |
4 | export class CreateUserDto {
5 | @IsString()
6 | @IsNotEmpty()
7 | @ApiProperty()
8 | readonly username: string;
9 |
10 | @IsString()
11 | @IsNotEmpty()
12 | @ApiProperty()
13 | readonly password: string;
14 | }
15 |
16 | export class UpdateUserDto {
17 | @IsString()
18 | @IsOptional()
19 | @ApiPropertyOptional()
20 | readonly username?: string;
21 |
22 | @IsString()
23 | @IsOptional()
24 | @ApiPropertyOptional()
25 | readonly password?: string;
26 | }
27 |
28 | export class UserPresentationDto {
29 | @ApiProperty()
30 | id: string;
31 |
32 | @ApiProperty()
33 | username: string;
34 |
35 | @ApiProperty()
36 | password: string;
37 |
38 | @ApiProperty()
39 | isDeleted: boolean;
40 | }
41 |
--------------------------------------------------------------------------------
/src/users/errors/errors.ts:
--------------------------------------------------------------------------------
1 | export class UserNotFoundError extends Error {
2 | constructor(id: string) {
3 | super(`User ${id} not found`);
4 | this.name = 'UserNotFoundError';
5 | }
6 | }
7 |
8 | export class UserAlreadyExistsError extends Error {
9 | constructor(username: string) {
10 | super(`User ${username} already exists`);
11 | this.name = 'UserAlreadyExistsError';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/users/index.ts:
--------------------------------------------------------------------------------
1 | export * from './errors/errors';
2 | export * from './interfaces/interfaces';
3 | export * from './models/user.model';
4 | export * from './schema/user.schema';
5 | export { UsersController } from './users.controller';
6 | export { UsersModule } from './users.module';
7 | export { UsersMapper, UsersRepository } from './users.repository';
8 | export { UsersService } from './users.service';
9 |
--------------------------------------------------------------------------------
/src/users/interfaces/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { UserForCreate, User, UserForUpdate, AuthenticatedUser } from '../models/user.model';
2 | import { Either } from 'monet';
3 | import { UserNotFoundError, UserAlreadyExistsError } from '../errors/errors';
4 | import { RoleRelationNotFoundError } from '../../user-role-rel';
5 | import { RoleNotFoundError } from '../../roles';
6 |
7 | export type QueryParams = {
8 | username?: string;
9 | password?: string;
10 | isDeleted?: boolean;
11 | };
12 |
13 | export interface IUsersRepository {
14 | create(user: UserForCreate): Promise;
15 |
16 | getAll(query?: QueryParams): Promise;
17 |
18 | findOne(id: string): Promise>;
19 |
20 | update(id: string, patch: UserForUpdate): Promise>;
21 |
22 | delete(id: string): Promise>;
23 | }
24 |
25 | export interface IUsersMapper {
26 | fromEntity(entity: any): User;
27 | }
28 |
29 | export interface IUsersService {
30 | getUserCredentials(
31 | username: string,
32 | password: string,
33 | ): Promise>;
34 |
35 | getAll(query?: QueryParams): Promise;
36 |
37 | findOne(id: string): Promise>;
38 |
39 | addUser(user: UserForCreate): Promise>;
40 | updateUser(id: string, patch: UserForUpdate): Promise>;
41 | removeUser(id: string): Promise>;
42 | }
43 |
--------------------------------------------------------------------------------
/src/users/models/user.model.ts:
--------------------------------------------------------------------------------
1 | export interface AuthenticatedUser {
2 | readonly username: string;
3 | readonly roles: string[];
4 | }
5 |
6 | export type User = {
7 | readonly id: string;
8 | username: string;
9 | password: string;
10 | isDeleted: boolean;
11 | };
12 |
13 | export type UserForCreate = Omit;
14 |
15 | export type UserForUpdate = {
16 | username?: string;
17 | password?: string;
18 | };
19 |
--------------------------------------------------------------------------------
/src/users/schema/user.schema.ts:
--------------------------------------------------------------------------------
1 | import { Schema, Document, connection } from 'mongoose';
2 | import { USER_MODEL } from '../../constants';
3 |
4 | export interface UserDocument extends Document {
5 | readonly username: string;
6 | readonly password: string;
7 | }
8 |
9 | export const UserSchema: Schema = new Schema({
10 | username: { type: String, required: true, unique: true },
11 | password: { type: String, required: true },
12 | isDeleted: { type: Boolean, default: false },
13 | });
14 |
15 | export const usersModel = connection.model(USER_MODEL, UserSchema, 'identity-users');
16 |
--------------------------------------------------------------------------------
/src/users/users.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Get,
4 | Post,
5 | Param,
6 | Body,
7 | Patch,
8 | Delete,
9 | Inject,
10 | HttpStatus,
11 | NotFoundException,
12 | ConflictException,
13 | Query,
14 | } from '@nestjs/common';
15 | import {
16 | ApiTags,
17 | ApiResponse,
18 | ApiBasicAuth,
19 | ApiOperation,
20 | ApiParam,
21 | ApiForbiddenResponse,
22 | ApiNotFoundResponse,
23 | ApiConflictResponse,
24 | ApiQuery,
25 | } from '@nestjs/swagger';
26 | import { CreateUserDto, UpdateUserDto, UserPresentationDto } from './dto/user.dto';
27 | import { Roles } from '../auth';
28 | import { IUsersService } from './interfaces/interfaces';
29 | import { USERS_SERVICE } from '../constants';
30 | import { identity } from 'rxjs';
31 | import { ObjectIdValidationPipe } from '../object-id.validation.pipe';
32 |
33 | @ApiBasicAuth()
34 | @ApiTags('users')
35 | @Controller('users')
36 | export class UsersController {
37 | constructor(
38 | @Inject(USERS_SERVICE)
39 | private readonly usersService: IUsersService,
40 | ) {}
41 |
42 | @Get()
43 | @Roles(['admin', 'guest'])
44 | @ApiResponse({ status: 200, type: [UserPresentationDto] })
45 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
46 | @ApiOperation({
47 | summary: 'Get users',
48 | description: 'Get all existing users',
49 | })
50 | @ApiQuery({ name: 'username', required: false })
51 | async getAll(@Query('query') query?: object): Promise {
52 | return this.usersService.getAll(query);
53 | }
54 |
55 | @Get(':id')
56 | @Roles(['admin', 'guest'])
57 | @ApiResponse({ status: 200, type: UserPresentationDto })
58 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
59 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
60 | @ApiParam({
61 | name: 'id',
62 | type: String,
63 | example: 'id: 5e5eb0418aa9340f913008e5',
64 | })
65 | @ApiOperation({
66 | summary: 'Get user',
67 | description: 'Get specific user by id',
68 | })
69 | async findOne(@Param('id', ObjectIdValidationPipe) id: string): Promise {
70 | const result = await this.usersService.findOne(id);
71 | return result.cata(error => {
72 | throw new NotFoundException({
73 | status: HttpStatus.NOT_FOUND,
74 | error: error.name,
75 | message: error.message,
76 | });
77 | }, identity);
78 | }
79 |
80 | @Post()
81 | @Roles(['admin'])
82 | @ApiResponse({ status: 201, type: UserPresentationDto })
83 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
84 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
85 | @ApiConflictResponse({ status: 409, description: 'User already exists' })
86 | @ApiOperation({
87 | summary: 'Add new user',
88 | description: 'Create new user',
89 | })
90 | async createUser(@Body() createUserDto: CreateUserDto): Promise {
91 | const result = await this.usersService.addUser(createUserDto);
92 | return result.cata(error => {
93 | throw new ConflictException({
94 | status: HttpStatus.CONFLICT,
95 | error: error.name,
96 | message: error.message,
97 | });
98 | }, identity);
99 | }
100 |
101 | @Patch(':id')
102 | @Roles(['admin'])
103 | @ApiResponse({ status: 200, type: UserPresentationDto })
104 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
105 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
106 | @ApiParam({
107 | name: 'id',
108 | type: String,
109 | example: 'id: 5e5eb0418aa9340f913008e5',
110 | })
111 | @ApiOperation({
112 | summary: 'Update user',
113 | description: 'Update specific user by id',
114 | })
115 | async updateUser(
116 | @Param('id', ObjectIdValidationPipe) id: string,
117 | @Body() updateUserDto: UpdateUserDto,
118 | ): Promise {
119 | const result = await this.usersService.updateUser(id, updateUserDto);
120 | return result.cata(error => {
121 | throw new NotFoundException({
122 | status: HttpStatus.NOT_FOUND,
123 | error: error.name,
124 | message: error.message,
125 | });
126 | }, identity);
127 | }
128 |
129 | @Delete(':id')
130 | @Roles(['admin'])
131 | @ApiResponse({ status: 200, type: UserPresentationDto })
132 | @ApiForbiddenResponse({ status: 403, description: 'Forbidden' })
133 | @ApiNotFoundResponse({ status: 404, description: 'Not Found' })
134 | @ApiParam({
135 | name: 'id',
136 | type: String,
137 | example: 'id: 5e5eb0418aa9340f913008e5',
138 | })
139 | @ApiOperation({
140 | summary: 'Remove user',
141 | description: 'Delete specific user by id',
142 | })
143 | async removeUser(@Param('id', ObjectIdValidationPipe) id: string): Promise {
144 | const result = await this.usersService.removeUser(id);
145 | return result.cata(error => {
146 | throw new NotFoundException({
147 | status: HttpStatus.NOT_FOUND,
148 | error: error.name,
149 | message: error.message,
150 | });
151 | }, identity);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, DynamicModule, Inject, NestModule, MiddlewareConsumer } from '@nestjs/common';
2 | import { UsersController } from './users.controller';
3 | import { IUsersService } from './interfaces/interfaces';
4 | import { IRolesService } from '../roles/';
5 | import { IUserRoleRelService } from '../user-role-rel';
6 | import {
7 | ROLES_SERVICE,
8 | USER_ROLE_RELATION_SERVICE,
9 | USERS_SERVICE,
10 | AUTHENTICATE,
11 | } from '../constants';
12 | import { Authenticate } from '../auth';
13 |
14 | @Module({})
15 | export class UsersModule implements NestModule {
16 | static forRoot(dependencies: {
17 | usersService: IUsersService;
18 | rolesService: IRolesService;
19 | userRoleRelService: IUserRoleRelService;
20 | authenticate: Authenticate;
21 | }): DynamicModule {
22 | return {
23 | module: UsersModule,
24 | controllers: [UsersController],
25 | providers: [
26 | {
27 | provide: USERS_SERVICE,
28 | useValue: dependencies.usersService,
29 | },
30 | {
31 | provide: ROLES_SERVICE,
32 | useValue: dependencies.rolesService,
33 | },
34 | {
35 | provide: USER_ROLE_RELATION_SERVICE,
36 | useValue: dependencies.userRoleRelService,
37 | },
38 | {
39 | provide: AUTHENTICATE,
40 | useValue: dependencies.authenticate,
41 | },
42 | ],
43 | };
44 | }
45 |
46 | constructor(@Inject(AUTHENTICATE) private readonly authenticate: Authenticate) {}
47 |
48 | configure(consumer: MiddlewareConsumer) {
49 | consumer.apply(this.authenticate).forRoutes(UsersController);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/users/users.repository.ts:
--------------------------------------------------------------------------------
1 | import { UserForCreate, User, UserForUpdate } from './models/user.model';
2 | import { UserDocument } from './schema/user.schema';
3 | import { Model } from 'mongoose';
4 | import { QueryParams, IUsersMapper, IUsersRepository } from './interfaces/interfaces';
5 | import { Either, Left, Right } from 'monet';
6 | import { UserNotFoundError } from './errors/errors';
7 |
8 | export class UsersMapper implements IUsersMapper {
9 | fromEntity(entity: any): User {
10 | return {
11 | id: entity._id.toString(),
12 | username: entity.username,
13 | password: entity.password,
14 | isDeleted: entity.isDeleted,
15 | };
16 | }
17 | }
18 |
19 | export class UsersRepository implements IUsersRepository {
20 | constructor(private readonly database: Model, private mapper: IUsersMapper) {}
21 |
22 | async create(user: UserForCreate): Promise {
23 | const createdUser = await this.database.create(user);
24 | return this.mapper.fromEntity(createdUser);
25 | }
26 |
27 | async getAll(query?: QueryParams): Promise {
28 | let users;
29 |
30 | if (query) {
31 | users = await this.database.find(query);
32 | } else {
33 | users = await this.database.find();
34 | }
35 |
36 | return users.map(this.mapper.fromEntity);
37 | }
38 |
39 | async findOne(id: string): Promise> {
40 | const user = await this.database.findById(id);
41 |
42 | if (!user) {
43 | return Left(new UserNotFoundError(id));
44 | }
45 | return Right(this.mapper.fromEntity(user));
46 | }
47 |
48 | async update(id: string, patch: UserForUpdate): Promise> {
49 | const eitherGetUser = await this.findOne(id);
50 |
51 | if (eitherGetUser.isLeft()) {
52 | return eitherGetUser;
53 | }
54 |
55 | const updated = await this.database.findByIdAndUpdate(id, patch, {
56 | new: true,
57 | });
58 | return Right(this.mapper.fromEntity(updated));
59 | }
60 |
61 | async delete(id: string): Promise> {
62 | const eitherGetUser = await this.findOne(id);
63 |
64 | if (eitherGetUser.isLeft()) {
65 | return eitherGetUser;
66 | }
67 |
68 | const deleted = await this.database.findByIdAndUpdate(id, { isDeleted: true }, { new: true });
69 | return Right(this.mapper.fromEntity(deleted));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
2 | import { Either, Left, Right } from 'monet';
3 | import { ROLES_SERVICE, USER_ROLE_RELATION_SERVICE } from '../constants';
4 | import {
5 | IRolesService,
6 | Role,
7 | RoleAlreadyExistsError,
8 | RoleForCreate,
9 | RoleNotFoundError,
10 | } from '../roles';
11 | import { IUserRoleRelService, RoleRelationNotFoundError } from '../user-role-rel';
12 | import { UserAlreadyExistsError, UserNotFoundError } from './errors/errors';
13 | import { IUsersRepository, IUsersService, QueryParams } from './interfaces/interfaces';
14 | import { AuthenticatedUser, User, UserForCreate, UserForUpdate } from './models/user.model';
15 |
16 | @Injectable()
17 | export class UsersService implements IUsersService, OnModuleInit {
18 | constructor(
19 | private readonly usersRepo: IUsersRepository,
20 | @Inject(ROLES_SERVICE)
21 | private readonly rolesService: IRolesService,
22 | @Inject(USER_ROLE_RELATION_SERVICE)
23 | private readonly userRoleRelService: IUserRoleRelService,
24 | ) {}
25 |
26 | async onModuleInit() {
27 | const existingAdmin = await this.isUserAlreadyExists('admin');
28 |
29 | if (existingAdmin) {
30 | return;
31 | }
32 |
33 | const user: UserForCreate = {
34 | username: 'admin',
35 | password: '123',
36 | };
37 |
38 | const eitherCreate = await this.createAdminUser(user);
39 | return eitherCreate.map(() => {
40 | console.info(`Admin user created with password ${user.password}`);
41 | });
42 | }
43 |
44 | async getUserCredentials(
45 | username: string,
46 | password: string,
47 | ): Promise> {
48 | const query = { username, password };
49 | const [user] = await this.usersRepo.getAll(query);
50 |
51 | type GetRoleResult = Either;
52 | const eitherGetByAccount = await this.userRoleRelService.getByAccount(user.id);
53 | const eitherGetRole = await eitherGetByAccount.cata(
54 | async (error): Promise => Left(error),
55 | relations => {
56 | const [relation] = relations;
57 | return this.rolesService.findOne(relation.roleId);
58 | },
59 | );
60 |
61 | return eitherGetRole.map(role => ({
62 | username: user.username,
63 | roles: [role.name],
64 | }));
65 | }
66 |
67 | private async isUserAlreadyExists(username: string): Promise {
68 | const query = { username };
69 | const [user] = await this.usersRepo.getAll(query);
70 |
71 | return Boolean(user);
72 | }
73 |
74 | private async createAdminUser(
75 | user: UserForCreate,
76 | ): Promise> {
77 | const roleForCreate: RoleForCreate = {
78 | name: 'admin',
79 | displayName: 'ADMIN',
80 | description: 'admin role with access to all API routes',
81 | };
82 |
83 | const createdUser = await this.usersRepo.create(user);
84 | const eitherCreate = await this.rolesService.create(roleForCreate);
85 |
86 | eitherCreate.map(async role => {
87 | const relation = {
88 | userId: createdUser.id,
89 | roleId: role.id,
90 | };
91 |
92 | await this.userRoleRelService.create(relation);
93 | });
94 |
95 | return Right(createdUser);
96 | }
97 |
98 | async getAll(query?: QueryParams): Promise {
99 | return this.usersRepo.getAll(query);
100 | }
101 |
102 | async findOne(id: string): Promise> {
103 | return this.usersRepo.findOne(id);
104 | }
105 |
106 | async addUser(user: UserForCreate): Promise> {
107 | const { username } = user;
108 | const isUserExists = await this.isUserAlreadyExists(username);
109 |
110 | if (isUserExists) {
111 | return Left(new UserAlreadyExistsError(username));
112 | }
113 |
114 | const createdUser = await this.usersRepo.create(user);
115 | return Right(createdUser);
116 | }
117 |
118 | async updateUser(id: string, patch: UserForUpdate): Promise> {
119 | return this.usersRepo.update(id, patch);
120 | }
121 |
122 | async removeUser(id: string): Promise> {
123 | return this.usersRepo.delete(id);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/test.env:
--------------------------------------------------------------------------------
1 | MONGO_URL = mongodb://localhost:27017/nestjs-e2e
2 | NODE_ENV = test
3 | PORT = 5000
4 | URL = http://localhost
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es2017",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true,
13 | "noEmitOnError": true,
14 | "noImplicitAny": true,
15 | "strictNullChecks": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "noImplicitReturns": true
19 | },
20 | "exclude": ["node_modules", "dist"]
21 | }
22 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended"],
4 | "jsRules": {
5 | "no-unused-expression": true
6 | },
7 | "rules": {
8 | "quotemark": [true, "single", "avoid-escape"],
9 | "member-access": [false],
10 | "ordered-imports": [false],
11 | "max-line-length": [true, 150],
12 | "member-ordering": [false],
13 | "interface-name": [false],
14 | "arrow-parens": false,
15 | "object-literal-sort-keys": false,
16 | "interface-over-type-literal": false,
17 | "max-classes-per-file": false,
18 | "no-console": false,
19 | "no-implicit-dependencies": true,
20 | "object-literal-key-quotes": false,
21 | "semicolon": [true, "always", "strict-bound-class-methods"],
22 | "no-return-await": true,
23 | "array-type": false
24 | },
25 | "rulesDirectory": []
26 | }
27 |
--------------------------------------------------------------------------------