npm package: @outworld2/nest-core
Description: NestJs core package for apis.
Author: Maddoctor
Latest version: 1.3.2

# NestJs

Created At: Jan 20, 2021 10:55 AM
Destination: common
Environnement: Outworld-QA
Last Update: Feb 3, 2021 11:03 AM
Responsable: Arthur Pain
Type: Documentation
Univers: Technique

[![Build Status](https://dev.azure.com/enoviah/Enoviah/_apis/build/status/L-Nest-Core?branchName=master)](https://dev.azure.com/enoviah/Enoviah/_build/latest?definitionId=70&branchName=master)

# 1 - Guideline

Par défaut, Nest se base sur le système d'Angular. Ce qui implique que vous pouvez utiliser des modules, des providers, des controllers et des services.

Nest propose sont propre système d'injection semblable à celui d'Angular.

Vous pouvez accéder à la documentation [ici](https://docs.nestjs.com/).

## 1.1 - Structure

Voici la structure à garder en tête lors de la création d'une api. Elle peut être modifié en fonction des besoins. Mais pour un soucis de maintenabilité, veuillez la garder au maximum semblable à celle-ci.

```bash
--| src
	  --| modules
        --| users
            --| users.module.ts // Global module
            --| users.controller.ts // User controller (All access POST, GET, ...)
            --| user.providers.ts // User provider (Provide user Schema)
            --| user.service.ts // User method (Controller should call the service)
            --| schema
                --| user.schema.ts // Define user schema for mongoDb
            --| models
                --| user.dto.ts // Define all DTO of user
                --| user.model.ts // Define user model & user Document
    --| environment
    --| providers
    --| 
--| main.ts
--| app.module.ts
--| config.ts
```

# 2 - Configuration

Par défaut, vous devez appliquer le guide **Typescript** à votre api NestJs.

Votre api doit obligatoirement utiliser le modules **@enoviah/nest-core**

```bash
$> npm install --save @enoviah/nest-core:latest
```

# 3 - Nest-Core

Nest-core est le module interne à Outworld qui surcouche nestJs pour proposer différentes features.

## 3.1 - Environnement

Le module **nest-core** expose une gestion de l'environnement. Par défaut, dotenv est utilisé pour charger l’environnement.

### 3.1.1 - Chargement de l'environnement

Voici l'ordre de priorité:

1. .env
2. .env.example
3. environnement système

Si le .env n'existe pas, le .env.example sera chargé. Si il n'existe pas, l'environnement du système sera utilisé.

### 3.1.2 Configuration

**src/environment/** Location des fichiers liés à l'environnement

**env.model** Model de l'environnement

```tsx
interface Environment {
PORT: string;
}

export default Environment;
```

**env.ts** Export de l'environnement

```tsx
import { EnvironmentService } from '@enoviah/nest-core';
import Environment from './env.model';

const environment = new EnvironmentService<Environment>();
environment.loadEnvironment(false);
export default environment;
```

Optionnel **env.ts** Validation de l'environnement avec un schéma

```tsx
import { EnvironmentService } from '@enoviah/nest-core';
import Environment from './env.model';
import envSchema from './env.schema';

const environment = new EnvironmentService<Environment>();
environment.validators = envSchema; // Assignation du schéma
environment.loadEnvironment(true);
export default environment;
```

Optionnel **env.schema.ts** JsonSchema de l'environnement

```tsx
import { Schema } from 'jsonschema';

const envSchema: Schema = {
  id: '/Env',
  type: 'object',
  properties: {
    PORT: { type: 'string' },
  },
  required: ['PORT'],
};
export default envSchema;
```

### 3.1.3 - Utilisation

Example d'utilisation

**/src/main.ts**

```tsx
import environment from './env/env';

console.log(environment.environment.PORT);
```

## 3.2 CRUD operations

Ce module expose un service qui va gérer les opérations basiques d'un controller (**C**reate, **R**ead, **U**pdate, **D**elete).

La class CRUD expose par défaut:

- **create**(): Create document
- **findMany**(): Find documents from query (Voir queries automatique)
- **findOne**(): Find a document
- **updateOne**(): Update a document
- **deleteOne**(): Delete a document

**user.service.ts**

```tsx
import { CRUD } from '@enoviah/nest-core';
import { Document, Model } from 'mongoose';
import { User, UserDocument } from './models/user.model';
import { Inject } from '@nestjs/common';
import { matchField } from '@enoviah/nest-core';

export class UsersService extends CRUD<User> {
  constructor(@Inject('USER_MODEL') private readonly usersModel: Model<UserDocument>) {
    super(usersModel);
    this.queryList.push({
      matcher: ['status'],
      handler: (key: string, value: string) => {
        return matchField({ status: value });
      },
      priority: 100,
    });
  }
}
```

## 3.3 - MongoDb

Nest-core expose un module de base de donnée

> Par défaut, Le module de bdd essaiera de vous connecter 3 fois. Si il ne parvient pas à établir une connexion, une erreur sera jetée

### 3.3.1 - Environnement

Vous devez définir votre environnement avec les variables demandées par le module:

```bash
MONGO_DATABASE=myDb
MONGO_ADDRESS=mongodb:27017
MONGO_USER=default // Do not use if auth is not defined on your database
MONGO_PASSWORD=default  // Do not use if auth is not defined on your database
```

### 3.3.2 - Utilisation

Exemple d'utilisation pour la collection **Users**.
**/src/app.module.ts**

```tsx
import { DatabaseModule } from '@enoviah/nest-core';
import { Module } from '@nestjs/common';

@Module({
  imports: [DatabaseModule.forRoot()],
  controllers: [],
  providers: [],
})
class AppModule {
}

export default AppModule;

```

**/src/modules/users**

**user.schema.ts** Définition du schéma

```tsx
import { Schema } from 'mongoose';
import ObjectId = Schema.Types.ObjectId;

const UserSchema = new Schema({
  firstName: { type: String, required: true },
  lastName: { type: String, required: true },
  email: { type: String, required: true },
  authId: { type: ObjectId, required: true },
}, { timestamps: true });

export default UserSchema;
```

**user.providers.ts**

```tsx
import { Provider } from '@nestjs/common';
import { Connection } from 'mongoose';
import UserSchema from './user.schema';

const providers: Provider[] = [{
  provide: 'USERS_MODEL',
  useFactory: (connection: Connection) => connection.model('Users', UserSchema),
  inject: ['DATABASE_CONNECTION'],
}];
export default providers;
```

**user.module.ts**

```tsx
import {
  DatabaseModule,
} from '@enoviah/nest-core';
import { Module } from '@nestjs/common';
import userProviders from './users.providers';
import UsersService from './users.service';

@Module({
  imports: [],
  controllers: [],
  providers: [...userProviders, UsersService],
  exports: [UsersService],
})
class UsersModule {
}

export default UsersModule;
```

**user.service.ts**

```tsx
import {Model} from 'mongoose';
import {
  Inject,
  Injectable,
} from '@nestjs/common';

@Injectable()
class UsersService {
  constructor(@Inject('USERS_MODEL') private readonly userModel: Model<UserDocument>) {
  }
}
```

## 3.4 - Queries automatique

Vous pouvez utiliser le système de queries automatique.

Il y a deux modes :

- Pagination
- Sans pagination

Ces deux modes peuvent être activé grâce à l'objet de configuration.

Par défaut, la class **Query** expose deux queries:

- limit (Limite des résultats)
- offset (Début des résulats)

Si vous utilisez le mode pagination et que vous envoyez une de ces deux queries, elles sera appliquée.

Exemple avec une query possible sur la key **firstName** de **User**.

**user.service.ts**

```tsx
import {Model} from 'mongoose';
import {
  Inject,
  Injectable,
} from '@nestjs/common';
import { ExtractedQueries, Query, Stage, Utils, matchField } from '@enoviah/nest-core';

@Injectable()
class UsersService extends Query {
  constructor(@Inject('USERS_MODEL') private readonly userModel: Model<UserDocument>) {
		this.queryList.push({
		  matcher: ['firstName'],
		  handler: async (key: string, value: string) => {
		    const search = new RegExp('^' + value, 'ig');
		    return matchField({ firstName: search });
		  },
		  priority: 1,
		});
	}
	// Mode pagination
	getUsers(queries: Record<string, string>): Observable<{results: User[], count: number}> {
		return this.extractQuery(queries, {count: true)
      .pipe(mergeMap((queries: ExtractedQueries[]) => {
        return from(this.userModel.aggregate([queries]).exec()).pipe(Utils.mapAggregateCount()) 
				};
        }));
      }));
	}
  // Mode sans pagination
	getUsers(queries: Record<string, string>): Observable<User[]> {
		return this.extractQuery(queries, {})
      .pipe(mergeMap((queries: ExtractedQueries[]) => {
        return from(this.userModel.aggregate([queries]).exec());
				};
        }));
      }));
	}
}
```

## 3.5 - Validation de données

Nest-core propose un service de validation de donnée basé sur **Json-Schema**.

**user.module.ts**

```tsx
import {
  DatabaseModule,
	JsonSchemaModule,
} from '@enoviah/nest-core';
import { Module } from '@nestjs/common';
import userProviders from './users.providers';
import UsersService from './users.service';

@Module({
  imports: [DatabaseModule, JsonSchemaModule],
  controllers: [],
  providers: [...userProviders, UsersService],
  exports: [UsersService],
})
class UsersModule {
}

export default UsersModule;
```

**user.service.ts**

```tsx
import {
  AuthUser,
  BadRequest,
  JsonSchemaService,
} from '@enoviah/nest-core';
import {
  BadRequestException,
  Inject,
  Injectable,
} from '@nestjs/common';
import {
  Model,
} from 'mongoose';
import {
  catchError,
  mergeMap,
} from 'rxjs/operators';
import { UserDocument } from '../../models/user/user.document';
import CreateUserRequest from '../../models/user/user.dto';
import creatUserSchema from '../../models/user/user.validations';

@Injectable()
class UsersService {
  constructor(@Inject('USERS_MODEL') private readonly userModel: Model<UserDocument>,
    private readonly jsonSchemaService: JsonSchemaService) {
  }

  create(body: CreateUserRequest): Observable<UserDocument> {
	// Will automatically throw a 400 bad request if the validation fail
    this.jsonSchemaService.validate(body, creatUserSchema, true);
   }
}

export default UsersService;
```

## 3.6 - Gestion d'erreur

Nest-core propose une gestion des exceptions via un filtre.

Il agira sur les exceptions et les transformeras en une réponse http formatée.

**main.ts**

```tsx
**import { NestExpressApplication } from '@nestjs/platform-express';
import { EnvStatus } from '@enoviah/nest-core/dist/models/environment.model';
import { NestFactory } from '@nestjs/core';
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
import { HttpExceptionFilter, Utils } from '@enoviah/nest-core';
import * as helmet from 'helmet';
import { Logger } from '@nestjs/common';
import AppModule from './app.module';
import environment from './environment/env';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.use(helmet());
  app.use(cors({ origin: '*' }));
  app.use(bodyParser.json());
  
  // Register the filter
	app.useGlobalFilters(new HttpExceptionFilter());

  Logger.debug(`API listening on ${environment.environment.PORT}`);
  await app.listen(+environment.environment.PORT);
}

bootstrap();**
```

Si j'ai le code suivant:

**app.controller.ts**

```tsx
import {
  Controller,
  Get
} from '@nestjs/common';
import {
  BadRequest,
} from '@enoviah/nest-core';

@Controller('/')
class AppController {
  constructor() {
  }

  @Get('/500')
  async getAccount() {
	  throw new Error('Oops');
  }

  @Get('/400')
  async getAccount() {
		throw new BadRequest({ message: 'Passwords missmatch', code: 'EPASSWORDMISSMATCH' });  }
	}

export default AccountController;
```

les réponses http seront les suivantes:

```tsx
// /500
{
	"message": "Oops",
	"status": 500,
	"code": "EINTERNALESERVERERROR",
}
// /400
{
	"message": "Passwords missmatch",
  "code": "EPASSWORDMISSMATCH",
	"status": 400,
}
```

Une erreur inconnue sera aussi traitée par l’intercepteur et sera transformée en 500.

## 3.7 - Resources

Pour uniformiser la géstion des images / Vidéos, nest-core propose un module de gestion de ces derniers.

### 3.7.1 - Philosophie

Une **resource** est un document contenu dans la collection **Resources**. Il représente un fichier (qu'importe l'extension) stocké à un emplacement (Google Storage, Local, ...).

Ce module vous permet d'upload un fichier, de valider son contenu et de pouvoir générer l'url d'accés à n'importe quel moment.

### **3.7.2 - Utilisation**

Rentrons dans le vif du sujet.

D'abord, il faut définir les variables d’environnements suivantes :

**.env**

```bash
UPLOAD_LIMIT=1250000 // Taille max d'un fichier
UPLOAD_DIR=media // Dossier dans lequel les fichiers seront mis (/public/media)
```

Ensuite, il faut utiliser le module qui va exposer les router des resources

**app.module.ts**

```tsx
import { Module } from '@nestjs/common';
import { NestCoreModule, GmapsModule, ResourceModule, Utils } from '@enoviah/nest-core';

@Module({
  imports: [
    NestCoreModule,
    ResourceModule,
    ],
  controllers: [],
  providers: [],
})
export class AppModule {
}
```

Si vous avez bien configurer votre environnement, vous pouvez maintenant utiliser les routes. Voir l'api-docs pour la liste.

Si vous voulez pouvoir accéder aux resources via une url, il faut ajouter la ligne suivante:

**main.ts**

```tsx
function bootstrap() {
...
app.useStaticAssets(join(__dirname, 'public'));
}
```

**Example pour un utilisateur**

**user.schema.ts**

```tsx
import { Schema } from 'mongoose';
import ObjectId = Schema.Types.ObjectId;

export const UserSchema = new Schema({
  firstName: { type: String },
  lastName: { type: String },
  phoneNumber: { type: String },
  email: { type: String },
  avatar: {type: ObjectId, ref: 'Resources'} // On indique au schéma que c'est une ressource
}, { timestamps: true });
```

**user.controller.ts**

```tsx
import { Body, Controller, Get, Param, Post, Put, Req, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './model/user.model';
import {
  AuthenticatedRequest,
  BearerGuard,
  InternalErrorResponse,
  Roles,
  RolesGuard,
  UnauthorizedErrorResponse,
} from '@enoviah/nest-core';

@Controller('users')
@ApiTags('Users')
export class UserController {
  constructor(private readonly userService: UserService) {
  }

  @Get(':id')
  @UseGuards(BearerGuard)
  @UseInterceptors(ResourceInterceptor) // On utilise l'interceptor des resources
  @SetMetadata('resources', ['avatar']) // on indique ou se trouve la resource dans l'objet
  updateOne(@Param('id') id: string) {
    return this.userService.findOne(id).pipe(mergeMap((user) => {
			return from(user.populate('avatar').execPopulate());
		}));
  }
}
```

On obtient après cet appel :

```json
GET /USERS/:id
{
 ...,
 "avatar": {
	"_id": "string",
  "url": "https://xxxx/public/resources/xxx.png",
 }
} 
```

### 3.7.3 - Resource service

Vous pouvez accéder à ResourceService pour directement agir sur une resource.

## 3.8 - Google maps

Vous pouvez utiliser le controller Google maps de nest-core.

Il vous permet de :

- D'auto-compléter une adresse en France
- D'obtenir les coordonnées d'une adresse
- D'obtenir les informations d'une *place*
- D'obtenir les *places* autour d'une coordonnée

### 3.8.1 - Utilisation

Comme pour les autres modules, il vous suffit de l'importer dans votre module et de définir votre clef google.

**Attention**: Pensez à activer les différentes API sur votre panel Google.

**.env**

```bash
GMAPS_API_KEY=azeazxxxxazefezfzefez-azes
```

**app.module.ts**

```tsx
import { Module } from '@nestjs/common';
import { NestCoreModule, GmapsModule, ResourceModule, Utils } from '@enoviah/nest-core';

@Module({
  imports: [
    NestCoreModule,
    GmapsModule,
    ],
  controllers: [],
  providers: [SyncGateway],
})
export class AppModule {
}
```

Vous pouvez maintenant utiliser les différentes routes.

##