From a2be64f054bf3c5c0b4a2c1886ce28354b5f9671 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Minelli?= <michael@minelli.me>
Date: Wed, 5 Jul 2023 17:50:50 +0200
Subject: [PATCH] Migrate to Prisma

---
 ExpressAPI/src/config/Config.ts               | 16 ----
 ExpressAPI/src/controllers/Session.ts         | 18 ++---
 ExpressAPI/src/helpers/DatabaseHelper.ts      | 38 +++++++--
 .../Prisma/Extensions/UserQueryExtension.ts   | 31 ++++++++
 .../Prisma/Extensions/UserResultExtension.ts  | 33 ++++++++
 ExpressAPI/src/managers/EnonceManager.ts      | 33 ++++----
 ExpressAPI/src/managers/GitlabManager.ts      |  2 +-
 ExpressAPI/src/managers/UserManager.ts        | 79 ++++++++-----------
 .../src/middlewares/ParamsCallbackManager.ts  | 16 +++-
 .../src/middlewares/SecurityMiddleware.ts     |  4 +-
 ExpressAPI/src/routes/EnonceRoutes.ts         | 59 ++++++++------
 ExpressAPI/src/routes/ExerciceRoutes.ts       | 71 +++++++++--------
 ExpressAPI/src/routes/SessionRoutes.ts        |  4 +-
 ExpressAPI/src/types/DatabaseTypes.ts         | 34 ++++++++
 14 files changed, 277 insertions(+), 161 deletions(-)
 create mode 100644 ExpressAPI/src/helpers/Prisma/Extensions/UserQueryExtension.ts
 create mode 100644 ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts
 create mode 100644 ExpressAPI/src/types/DatabaseTypes.ts

diff --git a/ExpressAPI/src/config/Config.ts b/ExpressAPI/src/config/Config.ts
index ae50eb1..1473320 100644
--- a/ExpressAPI/src/config/Config.ts
+++ b/ExpressAPI/src/config/Config.ts
@@ -1,6 +1,3 @@
-import Toolbox from '../shared/helpers/Toolbox';
-
-
 class Config {
     private static _instance: Config;
 
@@ -8,10 +5,6 @@ class Config {
         port: number
     };
 
-    public readonly database: {
-        type: string, host: string, port: number, user: string, password: string, database: string
-    };
-
     public jwtConfig: {
         secret: string; expiresIn: number;
     };
@@ -40,15 +33,6 @@ class Config {
             port: Number(process.env.API_PORT)
         };
 
-        this.database = {
-            type    : process.env.DATABASE_TYPE,
-            host    : process.env.DATABASE_HOST,
-            port    : Number(process.env.DATABASE_PORT),
-            user    : process.env.DATABASE_USER,
-            password: process.env.DATABASE_PASSWORD,
-            database: process.env.DATABASE_NAME
-        };
-
         this.jwtConfig = {
             secret   : process.env.JWT_SECRET_KEY,
             expiresIn: Number(process.env.SESSION_TIMEOUT)
diff --git a/ExpressAPI/src/controllers/Session.ts b/ExpressAPI/src/controllers/Session.ts
index aa6fdd3..3abcc18 100644
--- a/ExpressAPI/src/controllers/Session.ts
+++ b/ExpressAPI/src/controllers/Session.ts
@@ -3,20 +3,21 @@ import * as jwt            from 'jsonwebtoken';
 import { JwtPayload }      from 'jsonwebtoken';
 import Config              from '../config/Config';
 import express             from 'express';
-import ApiRequest          from '../models/ApiRequest';
-import User                from '../models/User';
+import ApiRequest          from '../types/ApiRequest';
 import UserManager         from '../managers/UserManager';
+import DojoResponse        from '../shared/types/DojoResponse';
+import { User }            from '../types/DatabaseTypes';
 
 
 class Session {
-    private _profile: User = new User();
+    private _profile: User;
 
     get profile(): User {
         return this._profile;
     }
 
     set profile(newProfile: User) {
-        newProfile.currentSession = this;
+        delete newProfile.password;
         this._profile = newProfile;
     }
 
@@ -31,9 +32,8 @@ class Session {
                 const jwtData = jwt.verify(jwtToken, Config.jwtConfig.secret) as JwtPayload;
 
                 if ( jwtData.profile ) {
-                    this.profile.importFromJsonObject(jwtData.profile);
-                    this.profile = await UserManager.getById(this.profile.userId);
-                    this.profile.currentSession = this;
+                    this.profile = jwtData.profile;
+                    this.profile = await UserManager.getById(this.profile.id);
                 }
             } catch ( err ) { }
         }
@@ -43,8 +43,8 @@ class Session {
         return profileJson.id === null ? null : jwt.sign({ profile: profileJson }, Config.jwtConfig.secret, Config.jwtConfig.expiresIn > 0 ? { expiresIn: Config.jwtConfig.expiresIn } : {});
     }
 
-    private async getResponse(code: number, data: any, descriptionOverride?: string): Promise<any> {
-        const profileJson = await this.profile.toJsonObject();
+    private async getResponse<T>(code: number, data: T, descriptionOverride?: string): Promise<DojoResponse<T>> {
+        const profileJson = this.profile;
 
         let reasonPhrase = '';
 
diff --git a/ExpressAPI/src/helpers/DatabaseHelper.ts b/ExpressAPI/src/helpers/DatabaseHelper.ts
index 9715876..89ec9c7 100644
--- a/ExpressAPI/src/helpers/DatabaseHelper.ts
+++ b/ExpressAPI/src/helpers/DatabaseHelper.ts
@@ -1,11 +1,35 @@
-import knex   from 'knex';
-import Config from '../config/Config';
+import { PrismaClient }    from '@prisma/client';
+import logger              from '../shared/logging/WinstonLogger';
+import UserQueryExtension  from './Prisma/Extensions/UserQueryExtension';
+import UserResultExtension from './Prisma/Extensions/UserResultExtension';
 
 
-const db = knex({
-                    client    : Config.database.type,
-                    connection: Config.database
-                });
+const prisma = new PrismaClient({
+                                    log: [ {
+                                        emit : 'event',
+                                        level: 'query'
+                                    }, {
+                                        emit : 'event',
+                                        level: 'info'
+                                    }, {
+                                        emit : 'event',
+                                        level: 'warn'
+                                    }, {
+                                        emit : 'event',
+                                        level: 'error'
+                                    } ]
+                                });
 
+prisma.$on('query', e => {
+    logger.debug(`Prisma => Query (${ e.duration }ms): ${ e.query }`);
+    logger.debug(`Prisma => Params: ${ e.params }\n`);
+});
+prisma.$on('info', e => logger.info(`Prisma => ${ e.message }`));
+prisma.$on('warn', e => logger.warn(`Prisma => ${ e.message }`));
+prisma.$on('error', e => logger.error(`Prisma => ${ e.message }`));
 
-export default db;
+
+const db = prisma.$extends(UserQueryExtension).$extends(UserResultExtension);
+
+
+export default db;
\ No newline at end of file
diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserQueryExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserQueryExtension.ts
new file mode 100644
index 0000000..0627c77
--- /dev/null
+++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserQueryExtension.ts
@@ -0,0 +1,31 @@
+import { Prisma } from '@prisma/client';
+
+
+export default Prisma.defineExtension(client => {
+    return client.$extends({
+                               query: {
+                                   user: {
+                                       async delete({
+                                                        args
+                                                    }) {
+
+                                           return client.user.update(Object.assign(args, {
+                                               data: {
+                                                   deleted: true
+                                               }
+                                           }));
+                                       },
+                                       async deleteMany({
+                                                            args
+                                                        }) {
+
+                                           return client.user.updateMany(Object.assign(args, {
+                                               data: {
+                                                   deleted: true
+                                               }
+                                           }));
+                                       }
+                                   }
+                               }
+                           });
+});
\ No newline at end of file
diff --git a/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts
new file mode 100644
index 0000000..604b6de
--- /dev/null
+++ b/ExpressAPI/src/helpers/Prisma/Extensions/UserResultExtension.ts
@@ -0,0 +1,33 @@
+import { Prisma }    from '@prisma/client';
+import Config        from '../../../config/Config';
+import LazyVal       from '../../../shared/helpers/LazyVal';
+import GitlabUser    from '../../../shared/types/Gitlab/GitlabUser';
+import GitlabManager from '../../../managers/GitlabManager';
+
+
+export default Prisma.defineExtension(client => {
+    return client.$extends({
+                               result: {
+                                   user: {
+                                       isTeachingStaff: {
+                                           needs: {
+                                               role: true
+                                           },
+                                           compute(user) {
+                                               return Config.permissions.teachingStaff.includes(user.role);
+                                           }
+                                       },
+                                       gitlabProfile  : {
+                                           needs: {
+                                               gitlabId: true
+                                           },
+                                           compute(user) {
+                                               return new LazyVal<GitlabUser>(() => {
+                                                   return GitlabManager.getUserById(user.gitlabId);
+                                               });
+                                           }
+                                       }
+                                   }
+                               }
+                           });
+});
\ No newline at end of file
diff --git a/ExpressAPI/src/managers/EnonceManager.ts b/ExpressAPI/src/managers/EnonceManager.ts
index 03022d8..1264464 100644
--- a/ExpressAPI/src/managers/EnonceManager.ts
+++ b/ExpressAPI/src/managers/EnonceManager.ts
@@ -1,5 +1,6 @@
-import db     from '../helpers/DatabaseHelper';
-import Enonce from '../models/Enonce';
+import { Prisma } from '@prisma/client';
+import { Enonce } from '../types/DatabaseTypes';
+import db         from '../helpers/DatabaseHelper';
 
 
 class EnonceManager {
@@ -15,30 +16,24 @@ class EnonceManager {
         return EnonceManager._instance;
     }
 
-    createObjectFromRawSql(raw: any): Enonce {
-        const enonce = Enonce.createFromSql(raw);
-
-        enonce.enonceGitlabCreationInfo = raw.enonceGitlabCreationInfo;
-        enonce.enonceGitlabLastInfo = raw.enonceGitlabLastInfo;
-
-        return enonce;
-    }
-
-    async getByName(name: string): Promise<Enonce | undefined> {
-        const raw = await db<Enonce>(Enonce.tableName).where('enonceName', name).first();
-
-        return raw ? this.createObjectFromRawSql(raw) : undefined;
+    async getByName(name: string, include: Prisma.EnonceInclude | undefined = undefined): Promise<Enonce | undefined> {
+        return db.enonce.findUnique({
+                                        where  : {
+                                            name: name
+                                        },
+                                        include: include
+                                    });
     }
 
-    getByGitlabLink(gitlabLink: string): Promise<Enonce | undefined> {
+    getByGitlabLink(gitlabLink: string, include: Prisma.EnonceInclude | undefined = undefined): Promise<Enonce | undefined> {
         const name = gitlabLink.replace('.git', '').split('/').pop();
 
-        return this.getByName(name);
+        return this.getByName(name, include);
     }
 
-    get(nameOrUrl: string): Promise<Enonce | undefined> {
+    get(nameOrUrl: string, include: Prisma.EnonceInclude | undefined = undefined): Promise<Enonce | undefined> {
         // We can use the same function for both name and url because the name is the last part of the url and the name extraction from the url doesn't corrupt the name
-        return this.getByGitlabLink(nameOrUrl);
+        return this.getByGitlabLink(nameOrUrl, include);
     }
 }
 
diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts
index d1a6d5d..8474c7d 100644
--- a/ExpressAPI/src/managers/GitlabManager.ts
+++ b/ExpressAPI/src/managers/GitlabManager.ts
@@ -113,7 +113,7 @@ class GitlabManager {
         };
         members.forEach(member => {
             if ( member.access_level >= GitlabAccessLevel.REPORTER ) {
-                if ( member.id === req.session.profile.userGitlabId ) {
+                if ( member.id === req.session.profile.gitlabId ) {
                     isUsersAtLeastReporter.user = true;
                 } else if ( member.id === Config.gitlab.account.id ) {
                     isUsersAtLeastReporter.dojo = true;
diff --git a/ExpressAPI/src/managers/UserManager.ts b/ExpressAPI/src/managers/UserManager.ts
index 748ca4c..5456838 100644
--- a/ExpressAPI/src/managers/UserManager.ts
+++ b/ExpressAPI/src/managers/UserManager.ts
@@ -1,7 +1,7 @@
-import User        from '../models/User';
-import db          from '../helpers/DatabaseHelper';
-import GitlabUser  from '../shared/types/Gitlab/GitlabUser';
-import EnonceStaff from '../models/EnonceStaff';
+import GitlabUser from '../shared/types/Gitlab/GitlabUser';
+import { Prisma } from '@prisma/client';
+import db         from '../helpers/DatabaseHelper';
+import { User }   from '../types/DatabaseTypes';
 
 
 class UserManager {
@@ -17,59 +17,50 @@ class UserManager {
         return UserManager._instance;
     }
 
-    createObjectFromRawSql(raw: any): User {
-        return User.createFromSql(raw);
+    async getByMail(mail: string, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> {
+        return db.user.findUnique({
+                                      where  : {
+                                          mail: mail
+                                      },
+                                      include: include
+                                  });
     }
 
-    async getByMail(mail: string): Promise<User | undefined> {
-        const raw = await db<User>(User.tableName).where('userMail', mail).first();
-
-        return raw ? this.createObjectFromRawSql(raw) : undefined;
-    }
-
-    async getById(id: number): Promise<User | undefined> {
-        const raw = await db<User>(User.tableName).where('userId', id).first();
-
-        return raw ? this.createObjectFromRawSql(raw) : undefined;
-    }
-
-    async getByIds(ids: Array<number>): Promise<Array<User>> {
-        return Promise.all(ids.map(userId => this.getById(userId)));
+    async getById(id: number, include: Prisma.UserInclude | undefined = undefined): Promise<User | undefined> {
+        return await db.user.findUnique({
+                                            where  : {
+                                                id: id
+                                            },
+                                            include: include
+                                        });
     }
 
-    async getByGitlabId(gitlabId: number): Promise<User | number> {
-        const raw = await db<User>(User.tableName).where('userGitlabId', gitlabId).first();
-
-        return raw ? this.createObjectFromRawSql(raw) : gitlabId;
-    }
-
-    async getByGitlabIds(gitlabIds: Array<number>): Promise<Array<User | number>> {
-        return Promise.all(gitlabIds.map(gitlabId => this.getByGitlabId(gitlabId)));
+    async getByGitlabId(gitlabId: number, returnIdIfUndefined: boolean = true, include: Prisma.UserInclude | undefined = undefined): Promise<User | number | undefined> {
+        return (await db.user.findUnique({
+                                             where  : {
+                                                 gitlabId: gitlabId
+                                             },
+                                             include: include
+                                         })) ?? (returnIdIfUndefined ? gitlabId : undefined);
     }
 
-    async getFromGitlabUser(gitlabUser: GitlabUser, createIfNotExist: boolean = false): Promise<User | number> {
-        let user = await this.getByGitlabId(gitlabUser.id);
+    async getFromGitlabUser(gitlabUser: GitlabUser, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<User | number> {
+        let user = await this.getByGitlabId(gitlabUser.id, true, include);
 
         if ( typeof user === 'number' && createIfNotExist ) {
-            user = await User.createFromSql({
-                                                userFirstName: gitlabUser.name,
-                                                userGitlabId : gitlabUser.id
-                                            }).create();
+            user = (await db.user.create({
+                                             data: {
+                                                 firstname: gitlabUser.name,
+                                                 gitlabId : gitlabUser.id
+                                             }
+                                         })).id;
         }
 
         return user;
     }
 
-    async getFromGitlabUsers(gitlabUsers: Array<GitlabUser>, createIfNotExist: boolean = false): Promise<Array<User | number>> {
-        return Promise.all(gitlabUsers.map(gitlabUser => this.getFromGitlabUser(gitlabUser, createIfNotExist)));
-    }
-
-    async getStaffOfEnonce(enonceName: string): Promise<Array<User>> {
-        const raw = await db<User>(User.tableName)
-        .innerJoin(EnonceStaff.tableName, `${ EnonceStaff.tableName }.userId`, `${ User.tableName }.userId`)
-        .where('enonceName', enonceName);
-
-        return raw ? raw.map(user => this.createObjectFromRawSql(user)) : [];
+    async getFromGitlabUsers(gitlabUsers: Array<GitlabUser>, createIfNotExist: boolean = false, include: Prisma.UserInclude | undefined = undefined): Promise<Array<User | number>> {
+        return Promise.all(gitlabUsers.map(gitlabUser => this.getFromGitlabUser(gitlabUser, createIfNotExist, include)));
     }
 }
 
diff --git a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts
index 7626cde..4c09baa 100644
--- a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts
+++ b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts
@@ -1,5 +1,5 @@
 import { Express }     from 'express-serve-static-core';
-import ApiRequest      from '../models/ApiRequest';
+import ApiRequest      from '../types/ApiRequest';
 import express         from 'express';
 import { StatusCodes } from 'http-status-codes';
 import EnonceManager   from '../managers/EnonceManager';
@@ -18,9 +18,9 @@ class ParamsCallbackManager {
         return ParamsCallbackManager._instance;
     }
 
-    protected listenParam(paramName: string, backend: Express, context: any, functionName: string, indexName: string) {
+    protected listenParam(paramName: string, backend: Express, getFunction: (id: string | number, ...args: Array<any>) => Promise<any>, args: Array<any>, indexName: string) {
         backend.param(paramName, (req: ApiRequest, res: express.Response, next: express.NextFunction, id: string | number) => {
-            (context[functionName] as (id: string | number) => Promise<any>)(id).then(result => {
+            getFunction(id, ...args).then(result => {
                 if ( result ) {
                     this.initBoundParams(req);
                     (req.boundParams as any)[indexName] = result;
@@ -42,7 +42,15 @@ class ParamsCallbackManager {
     }
 
     register(backend: Express) {
-        this.listenParam('enonceNameOrUrl', backend, EnonceManager, 'get', 'enonce');
+        this.listenParam('enonceNameOrUrl',
+                         backend,
+                         EnonceManager.get.bind(EnonceManager),
+                         [ {
+                             exercices: true,
+                             staff: true
+                         } ],
+                         'enonce'
+        );
     }
 }
 
diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts
index cce4344..a488037 100644
--- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts
+++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts
@@ -2,7 +2,7 @@ import express           from 'express';
 import { StatusCodes }   from 'http-status-codes';
 import SecurityCheckType from '../types/SecurityCheckType';
 import logger            from '../shared/logging/WinstonLogger';
-import ApiRequest        from '../models/ApiRequest';
+import ApiRequest        from '../types/ApiRequest';
 
 
 class SecurityMiddleware {
@@ -22,7 +22,7 @@ class SecurityMiddleware {
     check(checkIfConnected: boolean, ...checkTypes: Array<SecurityCheckType>): (req: ApiRequest, res: express.Response, next: express.NextFunction) => void {
         return async (req: ApiRequest, res: express.Response, next: express.NextFunction) => {
             if ( checkIfConnected ) {
-                if ( req.session.profile.userId === null ) {
+                if ( req.session.profile.id === null ) {
                     return req.session.sendResponse(res, StatusCodes.UNAUTHORIZED);
                 }
             }
diff --git a/ExpressAPI/src/routes/EnonceRoutes.ts b/ExpressAPI/src/routes/EnonceRoutes.ts
index bd4e1db..1a58437 100644
--- a/ExpressAPI/src/routes/EnonceRoutes.ts
+++ b/ExpressAPI/src/routes/EnonceRoutes.ts
@@ -13,14 +13,12 @@ import Config                    from '../config/Config';
 import GitlabMember              from '../shared/types/Gitlab/GitlabMember';
 import GitlabAccessLevel         from '../shared/types/Gitlab/GitlabAccessLevel';
 import GitlabRepository          from '../shared/types/Gitlab/GitlabRepository';
-import UserManager               from '../managers/UserManager';
-import User                      from '../models/User';
-import Enonce                    from '../models/Enonce';
-import EnonceStaff               from '../models/EnonceStaff';
 import { AxiosError }            from 'axios';
 import logger                    from '../shared/logging/WinstonLogger';
 import DojoValidators            from '../helpers/DojoValidators';
-import EnonceManager             from '../managers/EnonceManager';
+import { Prisma }                from '@prisma/client';
+import db                        from '../helpers/DatabaseHelper';
+import { Enonce }                from '../types/DatabaseTypes';
 
 
 class EnonceRoutes implements RoutesManager {
@@ -60,11 +58,14 @@ class EnonceRoutes implements RoutesManager {
 
     // Get an enonce by its name or gitlab url
     private async getEnonce(req: ApiRequest, res: express.Response) {
-        return req.boundParams.enonce ? req.session.sendResponse(res, StatusCodes.OK, req.boundParams.enonce.toJsonObject()) : res.status(StatusCodes.NOT_FOUND).send();
+        return req.boundParams.enonce ? req.session.sendResponse(res, StatusCodes.OK, req.boundParams.enonce) : res.status(StatusCodes.NOT_FOUND).send();
     }
 
     private async createEnonce(req: ApiRequest, res: express.Response) {
         const params: { name: string, members: Array<GitlabUser>, template: string } = req.body;
+        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ];
+        params.members = params.members.removeObjectDuplicates(gitlabUser => gitlabUser.id);
+
 
         let repository: GitlabRepository;
         try {
@@ -83,31 +84,39 @@ class EnonceRoutes implements RoutesManager {
         }
 
         try {
-            const members: Array<GitlabMember | false> = await Promise.all([ req.session.profile.userGitlabId, ...params.members.map(member => member.id) ].map(async (memberId: number): Promise<GitlabMember | false> => {
+            await Promise.all(params.members.map(member => member.id).map(async (memberId: number): Promise<GitlabMember | false> => {
                 try {
-                    return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.Maintainer);
+                    return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER);
                 } catch ( e ) {
                     return false;
                 }
             }));
 
-            const enonce: Enonce = await EnonceManager.createObjectFromRawSql({
-                                                                                  enonceName              : repository.name,
-                                                                                  enonceGitlabId          : repository.id,
-                                                                                  enonceGitlabLink        : repository.web_url,
-                                                                                  enonceGitlabCreationInfo: JSON.stringify(repository),
-                                                                                  enonceGitlabLastInfo    : JSON.stringify(repository),
-                                                                                  enonceGitlabLastInfoTs  : new Date().getTime()
-                                                                              }).create();
-
-            let dojoUsers: Array<User> = [ req.session.profile, ...(await UserManager.getFromGitlabUsers(params.members, true) as Array<User>) ];
-            dojoUsers = dojoUsers.reduce((unique, user) => (unique.findIndex(uniqueUser => uniqueUser.userId === user.userId) !== -1 ? unique : [ ...unique, user ]), Array<User>());
-            await Promise.all(dojoUsers.map(dojoUser => EnonceStaff.createFromSql({
-                                                                                      enonceName: enonce.enonceName,
-                                                                                      userId    : dojoUser.userId
-                                                                                  }).create()));
-
-            return req.session.sendResponse(res, StatusCodes.OK, enonce.toJsonObject());
+            const enonce: Enonce = await db.enonce.create({
+                                                              data: {
+                                                                  name              : repository.name,
+                                                                  gitlabId          : repository.id,
+                                                                  gitlabLink        : repository.web_url,
+                                                                  gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
+                                                                  gitlabLastInfo    : repository as unknown as Prisma.JsonObject,
+                                                                  gitlabLastInfoDate: new Date(),
+                                                                  staff             : {
+                                                                      connectOrCreate: [ ...params.members.map(gitlabUser => {
+                                                                          return {
+                                                                              create: {
+                                                                                  gitlabId : gitlabUser.id,
+                                                                                  firstname: gitlabUser.name
+                                                                              },
+                                                                              where : {
+                                                                                  gitlabId: gitlabUser.id
+                                                                              }
+                                                                          };
+                                                                      }) ]
+                                                                  }
+                                                              }
+                                                          });
+
+            return req.session.sendResponse(res, StatusCodes.OK, enonce);
         } catch ( error ) {
             if ( error instanceof AxiosError ) {
                 return res.status(error.response.status).send();
diff --git a/ExpressAPI/src/routes/ExerciceRoutes.ts b/ExpressAPI/src/routes/ExerciceRoutes.ts
index 3f78320..840f4ed 100644
--- a/ExpressAPI/src/routes/ExerciceRoutes.ts
+++ b/ExpressAPI/src/routes/ExerciceRoutes.ts
@@ -10,18 +10,15 @@ import GitlabUser                from '../shared/types/Gitlab/GitlabUser';
 import GitlabManager             from '../managers/GitlabManager';
 import Config                    from '../config/Config';
 import GitlabRepository          from '../shared/types/Gitlab/GitlabRepository';
-import Enonce                    from '../models/Enonce';
 import { AxiosError }            from 'axios';
 import logger                    from '../shared/logging/WinstonLogger';
 import DojoValidators            from '../helpers/DojoValidators';
 import { v4 as uuidv4 }          from 'uuid';
 import GitlabMember              from '../shared/types/Gitlab/GitlabMember';
 import GitlabAccessLevel         from '../shared/types/Gitlab/GitlabAccessLevel';
-import User                      from '../models/User';
-import UserManager               from '../managers/UserManager';
-import Exercice                  from '../models/Exercice';
-import ExerciceMember            from '../models/ExerciceMember';
-import ExerciceManager           from '../managers/ExerciceManager';
+import { Prisma }                from '@prisma/client';
+import { Enonce, Exercice }      from '../types/DatabaseTypes';
+import db                        from '../helpers/DatabaseHelper';
 
 
 class ExerciceRoutes implements RoutesManager {
@@ -50,24 +47,26 @@ class ExerciceRoutes implements RoutesManager {
     }
 
     private getExerciceName(enonce: Enonce, members: Array<GitlabUser>, suffix: number): string {
-        return `DojoEx - ${ enonce.enonceName } - ${ members.map(member => member.username).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`;
+        return `DojoEx - ${ enonce.name } - ${ members.map(member => member.username).join(' + ') }${ suffix > 0 ? ` - ${ suffix }` : '' }`;
     }
 
     private getExercicePath(enonce: Enonce, exerciceId: string): string {
-        return `dojo-ex_${ enonce.enonceGitlabLastInfo.path }_${ exerciceId }`;
+        return `dojo-ex_${ (enonce.gitlabLastInfo as unknown as GitlabRepository).path }_${ exerciceId }`;
     }
 
     private async createExercice(req: ApiRequest, res: express.Response) {
         const params: { members: Array<GitlabUser> } = req.body;
-        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ];
+        params.members = [ await req.session.profile.gitlabProfile.value, ...params.members ].removeObjectDuplicates(gitlabUser => gitlabUser.id);
+        const enonce: Enonce = req.boundParams.enonce;
 
-        const exerciceId: string = uuidv4();
 
+        const exerciceId: string = uuidv4();
         let repository: GitlabRepository;
+
         let suffix: number = 0;
         do {
             try {
-                repository = await GitlabManager.forkRepository(req.boundParams.enonce.enonceGitlabCreationInfo.id, this.getExerciceName(req.boundParams.enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', req.boundParams.enonce.enonceName), Config.exercice.default.visibility, Config.gitlab.group.exercices);
+                repository = await GitlabManager.forkRepository((enonce.gitlabCreationInfo as unknown as GitlabRepository).id, this.getExerciceName(enonce, params.members, suffix), this.getExercicePath(req.boundParams.enonce, exerciceId), Config.exercice.default.description.replace('{{ENONCE_NAME}}', enonce.name), Config.exercice.default.visibility, Config.gitlab.group.exercices);
                 break;
             } catch ( error ) {
                 if ( error instanceof AxiosError ) {
@@ -87,33 +86,41 @@ class ExerciceRoutes implements RoutesManager {
         }
 
         try {
-            const members: Array<GitlabMember | false> = await Promise.all([ ...new Set([ ...(await req.boundParams.enonce.staff.value).map(member => member.userGitlabId), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => {
+            await Promise.all([ ...new Set([ ...enonce.staff.map(user => user.gitlabId), ...params.members.map(member => member.id) ]) ].map(async (memberId: number): Promise<GitlabMember | false> => {
                 try {
-                    return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.Maintainer);
+                    return await GitlabManager.addRepositoryMember(repository.id, memberId, GitlabAccessLevel.DEVELOPER);
                 } catch ( e ) {
                     return false;
                 }
             }));
 
-            const exercice: Exercice = await ExerciceManager.createObjectFromRawSql({
-                                                                                        exerciceId                : exerciceId,
-                                                                                        exerciceEnonceName        : req.boundParams.enonce.enonceName,
-                                                                                        exerciceName              : repository.name,
-                                                                                        exerciceGitlabId          : repository.id,
-                                                                                        exerciceGitlabLink        : repository.web_url,
-                                                                                        exerciceGitlabCreationInfo: JSON.stringify(repository),
-                                                                                        exerciceGitlabLastInfo    : JSON.stringify(repository),
-                                                                                        exerciceGitlabLastInfoTs  : new Date().getTime()
-                                                                                    }).create();
-
-            let dojoUsers: Array<User> = await UserManager.getFromGitlabUsers(params.members, true) as Array<User>;
-            dojoUsers = dojoUsers.reduce((unique, user) => (unique.findIndex(uniqueUser => uniqueUser.userId === user.userId) !== -1 ? unique : [ ...unique, user ]), Array<User>());
-            await Promise.all(dojoUsers.map(dojoUser => ExerciceMember.createFromSql({
-                                                                                         exerciceId: exercice.exerciceId,
-                                                                                         userId    : dojoUser.userId
-                                                                                     }).create()));
-
-            return req.session.sendResponse(res, StatusCodes.OK, exercice.toJsonObject());
+            const exercice: Exercice = await db.exercice.create({
+                                                                    data: {
+                                                                        id                : exerciceId,
+                                                                        enonceName        : enonce.name,
+                                                                        name              : repository.name,
+                                                                        gitlabId          : repository.id,
+                                                                        gitlabLink        : repository.web_url,
+                                                                        gitlabCreationInfo: repository as unknown as Prisma.JsonObject,
+                                                                        gitlabLastInfo    : repository as unknown as Prisma.JsonObject,
+                                                                        gitlabLastInfoDate: new Date(),
+                                                                        members           : {
+                                                                            connectOrCreate: [ ...params.members.map(gitlabUser => {
+                                                                                return {
+                                                                                    create: {
+                                                                                        gitlabId : gitlabUser.id,
+                                                                                        firstname: gitlabUser.name
+                                                                                    },
+                                                                                    where : {
+                                                                                        gitlabId: gitlabUser.id
+                                                                                    }
+                                                                                };
+                                                                            }) ]
+                                                                        }
+                                                                    }
+                                                                });
+
+            return req.session.sendResponse(res, StatusCodes.OK, exercice);
         } catch ( error ) {
             if ( error instanceof AxiosError ) {
                 return res.status(error.response.status).send();
diff --git a/ExpressAPI/src/routes/SessionRoutes.ts b/ExpressAPI/src/routes/SessionRoutes.ts
index b74019b..08c3f35 100644
--- a/ExpressAPI/src/routes/SessionRoutes.ts
+++ b/ExpressAPI/src/routes/SessionRoutes.ts
@@ -7,8 +7,8 @@ import RoutesManager             from '../express/RoutesManager';
 import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware';
 import ApiRequest                from '../types/ApiRequest';
 import UserManager               from '../managers/UserManager';
-import User                      from '../models/User';
 import SecurityMiddleware        from '../middlewares/SecurityMiddleware';
+import { User }                  from '../types/DatabaseTypes';
 
 
 class SessionRoutes implements RoutesManager {
@@ -46,7 +46,7 @@ class SessionRoutes implements RoutesManager {
         const user: User | undefined = await UserManager.getByMail(params.user);
 
         if ( user ) {
-            if ( bcrypt.compareSync(params.password, user.userPassword) ) {
+            if ( bcrypt.compareSync(params.password, user.password) ) {
                 req.session.profile = user;
 
                 req.session.sendResponse(res, StatusCodes.OK);
diff --git a/ExpressAPI/src/types/DatabaseTypes.ts b/ExpressAPI/src/types/DatabaseTypes.ts
new file mode 100644
index 0000000..a782d9a
--- /dev/null
+++ b/ExpressAPI/src/types/DatabaseTypes.ts
@@ -0,0 +1,34 @@
+import { Prisma } from '@prisma/client';
+import LazyVal    from '../shared/helpers/LazyVal';
+import GitlabUser from '../shared/types/Gitlab/GitlabUser';
+
+
+const userBase = Prisma.validator<Prisma.UserArgs>()({
+                                                         include: { exercices: true }
+                                                     });
+const enonceBase = Prisma.validator<Prisma.EnonceArgs>()({
+                                                             include: {
+                                                                 exercices: true,
+                                                                 staff    : true
+                                                             }
+                                                         });
+const exerciceBase = Prisma.validator<Prisma.ExerciceArgs>()({
+                                                                 include: {
+                                                                     members: true,
+                                                                     results: true
+                                                                 }
+                                                             });
+const resultBase = Prisma.validator<Prisma.ResultArgs>()({
+                                                             include: {
+                                                                 exercice: true
+                                                             }
+                                                         });
+
+
+export type User = Partial<Prisma.UserGetPayload<typeof userBase> & {
+    isTeachingStaff: boolean
+    gitlabProfile: LazyVal<GitlabUser>
+}>
+export type Enonce = Partial<Prisma.EnonceGetPayload<typeof enonceBase>>
+export type Exercice = Partial<Prisma.ExerciceGetPayload<typeof exerciceBase>>
+export type Result = Partial<Prisma.ResultGetPayload<typeof resultBase>>
\ No newline at end of file
-- 
GitLab