From 7abe517c8f051dc78bdabc7a2696da726ad6b539 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Sat, 28 Sep 2024 17:29:21 -0400 Subject: [PATCH] Quiz model class, using a Repository pattern with mocked repository and tests --- server/__tests__/quiz.test.ts | 125 ++++++++++++++++++++ server/config/db.ts | 18 +-- server/config/email.ts | 46 +++++--- server/models/quiz.ts | 153 +++++++----------------- server/package-lock.json | 162 +++++++++++++++++++++++--- server/package.json | 8 +- server/repositories/quizRepository.ts | 121 +++++++++++++++++++ server/tsconfig.json | 2 +- 8 files changed, 476 insertions(+), 159 deletions(-) create mode 100644 server/__tests__/quiz.test.ts create mode 100644 server/repositories/quizRepository.ts diff --git a/server/__tests__/quiz.test.ts b/server/__tests__/quiz.test.ts new file mode 100644 index 0000000..316f863 --- /dev/null +++ b/server/__tests__/quiz.test.ts @@ -0,0 +1,125 @@ +import { MongoClient, Db, ObjectId } from 'mongodb'; +import { Quiz } from '../models/quiz'; +import QuizRepository from '../repositories/quizRepository'; + +jest.mock('../repositories/quizRepository'); + +describe('Quiz Class', () => { + let connection: MongoClient; + let db: Db; + let quizRepository: QuizRepository; + + beforeAll(async () => { + connection = { + db: jest.fn().mockReturnThis(), + collection: jest.fn().mockReturnThis(), + findOne: jest.fn(), + insertOne: jest.fn(), + deleteOne: jest.fn(), + deleteMany: jest.fn(), + updateOne: jest.fn(), + } as unknown as MongoClient; + + db = connection.db(); + quizRepository = new QuizRepository(); + (quizRepository as any).db = { getConnection: () => db }; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create a new quiz', async () => { + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const insertedId = new ObjectId(); + jest.spyOn(quizRepository, 'createQuiz').mockResolvedValue(insertedId); + + const result = await quiz.create(); + + expect(result).toEqual(insertedId); + expect(quizRepository.createQuiz).toHaveBeenCalledWith(quiz); + }); + + it('should get the owner of a quiz', async () => { + const quizId = new ObjectId().toHexString(); + const userId = 'userId'; + jest.spyOn(quizRepository, 'getOwner').mockResolvedValue(userId); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.getOwner(quizId); + + expect(result).toEqual(userId); + expect(quizRepository.getOwner).toHaveBeenCalledWith(quizId); + }); + + it('should get the content of a quiz', async () => { + const quizId = new ObjectId().toHexString(); + const content = 'content'; + jest.spyOn(quizRepository, 'getContent').mockResolvedValue(content); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.getContent(quizId); + + expect(result).toEqual(content); + expect(quizRepository.getContent).toHaveBeenCalledWith(quizId); + }); + + it('should delete a quiz', async () => { + const quizId = new ObjectId().toHexString(); + jest.spyOn(quizRepository, 'delete').mockResolvedValue(true); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.delete(quizId); + + expect(result).toBe(true); + expect(quizRepository.delete).toHaveBeenCalledWith(quizId); + }); + + it('should update a quiz', async () => { + const quizId = new ObjectId().toHexString(); + const updateData = { title: 'new title' }; + jest.spyOn(quizRepository, 'update').mockResolvedValue(true); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.update(quizId, updateData); + + expect(result).toBe(true); + expect(quizRepository.update).toHaveBeenCalledWith(quizId, updateData); + }); + + it('should move a quiz to a new folder', async () => { + const quizId = new ObjectId().toHexString(); + const newFolderId = 'newFolderId'; + jest.spyOn(quizRepository, 'move').mockResolvedValue(true); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.move(quizId, newFolderId); + + expect(result).toBe(true); + expect(quizRepository.move).toHaveBeenCalledWith(quizId, newFolderId); + }); + + it('should duplicate a quiz', async () => { + const quizId = new ObjectId().toHexString(); + const newQuizId = new ObjectId(); + jest.spyOn(quizRepository, 'duplicate').mockResolvedValue(newQuizId); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.duplicate(quizId); + + expect(result).toEqual(newQuizId); + expect(quizRepository.duplicate).toHaveBeenCalledWith(quizId); + }); + + it('should check if a quiz exists', async () => { + const title = 'title'; + const userId = 'userId'; + jest.spyOn(quizRepository, 'quizExists').mockResolvedValue(true); + + const quiz = new Quiz('folderId', 'userId', 'title', 'content', quizRepository); + const result = await quiz.quizExists(title, userId); + + expect(result).toBe(true); + expect(quizRepository.quizExists).toHaveBeenCalledWith(title, userId); + }); +}); diff --git a/server/config/db.ts b/server/config/db.ts index cf492bf..8511960 100644 --- a/server/config/db.ts +++ b/server/config/db.ts @@ -1,22 +1,25 @@ -const { MongoClient } = require('mongodb'); -const dotenv = require('dotenv') +import { MongoClient, Db } from 'mongodb'; +import dotenv from 'dotenv'; dotenv.config(); class DBConnection { + private mongoURI: string; + private databaseName: string; + private connection: MongoClient | null; constructor() { - this.mongoURI = process.env.MONGO_URI; - this.databaseName = process.env.MONGO_DATABASE; + this.mongoURI = process.env.MONGO_URI || ''; + this.databaseName = process.env.MONGO_DATABASE || ''; this.connection = null; } - async connect() { + async connect(): Promise { const client = new MongoClient(this.mongoURI); this.connection = await client.connect(); } - getConnection() { + getConnection(): Db { if (!this.connection) { throw new Error('Connexion MongoDB non établie'); } @@ -24,5 +27,4 @@ class DBConnection { } } -const instance = new DBConnection(); -module.exports = instance; \ No newline at end of file +export default DBConnection; diff --git a/server/config/email.ts b/server/config/email.ts index e31c5f3..c12bbdb 100644 --- a/server/config/email.ts +++ b/server/config/email.ts @@ -1,15 +1,18 @@ -const nodemailer = require('nodemailer'); -const dotenv = require('dotenv'); +import nodemailer, { Transporter, SendMailOptions, SentMessageInfo } from 'nodemailer'; +import dotenv from 'dotenv'; dotenv.config(); class Emailer { + private senderEmail: string; + private psw: string; + private transporter: Transporter; constructor() { - this.senderEmail = process.env.SENDER_EMAIL; - this.psw = process.env.EMAIL_PSW; + this.senderEmail = process.env.SENDER_EMAIL || ''; + this.psw = process.env.EMAIL_PSW || ''; this.transporter = nodemailer.createTransport({ - service: process.env.EMAIL_SERVICE, + service: process.env.EMAIL_SERVICE || '', auth: { user: this.senderEmail, pass: this.psw @@ -17,33 +20,44 @@ class Emailer { }); } - registerConfirmation(email) { - this.transporter.sendMail({ + private handleEmailResult(error: Error | null, info: SentMessageInfo): void { + if (error) { + console.error('Error sending email:', error); + } else { + console.log('Email sent:', info.response); + } + } + + registerConfirmation(email: string): void { + const mailOptions: SendMailOptions = { from: this.senderEmail, to: email, subject: 'Confirmation de compte', - text: 'Votre compte a été créé avec succès.' - }); + // Add other email options here if needed + }; + this.transporter.sendMail(mailOptions, this.handleEmailResult); } - newPasswordConfirmation(email,newPassword) { - this.transporter.sendMail({ + newPasswordConfirmation(email: string, newPassword: string): void { + const mailOptions: SendMailOptions = { from: this.senderEmail, to: email, subject: 'Mot de passe temporaire', text: 'Votre nouveau mot de passe temporaire est : ' + newPassword - }); + }; + this.transporter.sendMail(mailOptions, this.handleEmailResult); } - quizShare(email, link) { - this.transporter.sendMail({ + quizShare(email: string, link: string): void { + const mailOptions: SendMailOptions = { from: this.senderEmail, to: email, subject: 'Un quiz vous a été transféré !', text: 'Veuillez suivre ce lien pour ajouter ce quiz à votre compte. '+ link - }); + }; + this.transporter.sendMail(mailOptions, this.handleEmailResult); } } -module.exports = new Emailer(); \ No newline at end of file +export default Emailer; diff --git a/server/models/quiz.ts b/server/models/quiz.ts index cb8f5a4..36a99f5 100644 --- a/server/models/quiz.ts +++ b/server/models/quiz.ts @@ -1,133 +1,58 @@ -const db = require('../config/db.js') -const { ObjectId } = require('mongodb'); +import { ObjectId } from 'mongodb'; +import QuizRepository from '../repositories/quizRepository'; -class Quiz { +export class Quiz { + private repository: QuizRepository; + folderId: string; + userId: string; + title: string; + content: string; + created_at: Date; + updated_at: Date; - async create(title, content, folderId, userId) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const existingQuiz = await quizCollection.findOne({ title: title, folderId: folderId, userId: userId }) - - if (existingQuiz) return null; - - const newQuiz = { - folderId: folderId, - userId: userId, - title: title, - content: content, - created_at: new Date(), - updated_at: new Date() - } - - const result = await quizCollection.insertOne(newQuiz); - - return result.insertedId; + constructor(folderId: string, userId: string, title: string, content: string, repository?: QuizRepository) { + this.repository = repository || new QuizRepository(); + this.folderId = folderId; + this.userId = userId; + this.title = title; + this.content = content; + this.created_at = new Date(); + this.updated_at = new Date(); } - async getOwner(quizId) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); - - return quiz.userId; + async create(): Promise { + return await this.repository.createQuiz(this); } - async getContent(quizId) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); - - return quiz; + async getOwner(quizId: string): Promise { + return await this.repository.getOwner(quizId); } - async delete(quizId) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const result = await quizCollection.deleteOne({ _id: new ObjectId(quizId) }); - - if (result.deletedCount != 1) return false; - - return true; - } - async deleteQuizzesByFolderId(folderId) { - await db.connect(); - const conn = db.getConnection(); - - const quizzesCollection = conn.collection('files'); - - // Delete all quizzes with the specified folderId - await quizzesCollection.deleteMany({ folderId: folderId }); + async getContent(quizId: string): Promise { + return await this.repository.getContent(quizId); } - async update(quizId, newTitle, newContent) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const result = await quizCollection.updateOne({ _id: new ObjectId(quizId) }, { $set: { title: newTitle, content: newContent } }); - //Ne fonctionne pas si rien n'est chngé dans le quiz - //if (result.modifiedCount != 1) return false; - - return true + async delete(quizId: string): Promise { + return await this.repository.delete(quizId); } - async move(quizId, newFolderId) { - await db.connect() - const conn = db.getConnection(); - - const quizCollection = conn.collection('files'); - - const result = await quizCollection.updateOne({ _id: new ObjectId(quizId) }, { $set: { folderId: newFolderId } }); - - if (result.modifiedCount != 1) return false; - - return true + async deleteQuizzes(folderId: string): Promise { + return await this.repository.deleteQuizzes(folderId); } - async duplicate(quizId, userId) { - - const sourceQuiz = await this.getContent(quizId); - - let newQuizTitle = `${sourceQuiz.title}-copy`; - let counter = 1; - while (await this.quizExists(newQuizTitle, userId)) { - newQuizTitle = `${sourceQuiz.title}-copy(${counter})`; - counter++; - } - //console.log(newQuizTitle); - const newQuizId = await this.create(newQuizTitle, sourceQuiz.content,sourceQuiz.folderId, userId); - - if (!newQuizId) { - throw new Error('Failed to create a duplicate quiz.'); - } - - return newQuizId; - + async update(quizId: string, updateData: Partial): Promise { + return await this.repository.update(quizId, updateData); } - async quizExists(title, userId) { - await db.connect(); - const conn = db.getConnection(); - - const filesCollection = conn.collection('files'); - const existingFolder = await filesCollection.findOne({ title: title, userId: userId }); - - return existingFolder !== null; + async move(quizId: string, newFolderId: string): Promise { + return await this.repository.move(quizId, newFolderId); } + async duplicate(quizId: string): Promise { + return await this.repository.duplicate(quizId); + } + + async quizExists(title: string, userId: string): Promise { + return await this.repository.quizExists(title, userId); + } } - -module.exports = new Quiz; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 2c84f91..3fba973 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -36,6 +36,7 @@ "jest": "^29.7.0", "nodemon": "^3.0.1", "supertest": "^6.3.4", + "ts-jest": "^29.2.5", "typescript": "^5.6.2" }, "engines": { @@ -1665,6 +1666,12 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1898,6 +1905,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2464,6 +2483,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.574", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.574.tgz", @@ -2767,6 +2801,36 @@ "bser": "2.1.1" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3443,6 +3507,24 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -4261,22 +4343,17 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -4292,6 +4369,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5142,12 +5225,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -5744,6 +5824,54 @@ "node": ">=14" } }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/server/package.json b/server/package.json index a71778d..c3d558a 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "build": "webpack --config webpack.config.js", "start": "node app.js", "dev": "nodemon app.js", - "test": "jest" + "test": "jest --colors" }, "keywords": [], "author": "", @@ -40,16 +40,18 @@ "jest": "^29.7.0", "nodemon": "^3.0.1", "supertest": "^6.3.4", + "ts-jest": "^29.2.5", "typescript": "^5.6.2" }, "engines": { "node": "18.x" }, "jest": { + "preset": "ts-jest", "testEnvironment": "node", "testMatch": [ - "**/__tests__/**/*.js?(x)", - "**/?(*.)+(spec|test).js?(x)" + "**/__tests__/**/*.ts?(x)", + "**/?(*.)+(spec|test).ts?(x)" ] } } diff --git a/server/repositories/quizRepository.ts b/server/repositories/quizRepository.ts new file mode 100644 index 0000000..f0c2c89 --- /dev/null +++ b/server/repositories/quizRepository.ts @@ -0,0 +1,121 @@ +import { Db, ObjectId } from 'mongodb'; +import DBConnection from '../config/db'; +import { Quiz } from '../models/quiz'; + +class QuizRepository { + private db: DBConnection; + + constructor() { + this.db = new DBConnection(); + } + + async createQuiz(quiz: Quiz): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const existingQuiz = await quizCollection.findOne({ title: quiz.title, folderId: quiz.folderId, userId: quiz.userId }); + + if (existingQuiz) return null; + + const result = await quizCollection.insertOne(quiz); + return result.insertedId; + } + + async getOwner(quizId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); + + return quiz ? quiz.userId : null; + } + + async getContent(quizId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); + + return quiz ? quiz.content : null; + } + + async delete(quizId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const result = await quizCollection.deleteOne({ _id: new ObjectId(quizId) }); + + return result.deletedCount === 1; + } + + async deleteQuizzes(folderId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const result = await quizCollection.deleteMany({ folderId: folderId }); + return result.deletedCount || 0; + } + + async update(quizId: string, updateData: Partial): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const result = await quizCollection.updateOne( + { _id: new ObjectId(quizId) }, + { $set: { ...updateData, updated_at: new Date() } } + ); + + return result.modifiedCount === 1; + } + + async move(quizId: string, newFolderId: string): Promise { + return await this.update(quizId, { folderId: newFolderId }); + } + + async duplicate(quizId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const quiz = await quizCollection.findOne({ _id: new ObjectId(quizId) }); + + if (!quiz) return null; + + const newQuiz = { + ...quiz, + _id: undefined, + created_at: new Date(), + updated_at: new Date() + }; + + const result = await quizCollection.insertOne(newQuiz); + + return result.insertedId; + } + + async quizExists(title: string, userId: string): Promise { + await this.db.connect(); + const conn: Db = this.db.getConnection(); + + const quizCollection = conn.collection('files'); + + const existingQuiz = await quizCollection.findOne({ title: title, userId: userId }); + + return existingQuiz !== null; + } +} + +export default QuizRepository; diff --git a/server/tsconfig.json b/server/tsconfig.json index 420e50e..6440104 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["**/*.ts"], + "include": ["./**/*.ts"], "exclude": ["node_modules", "dist"] }