Commit initial : version fonctionnelle du projet

This commit is contained in:
Mouktar Kimba
2025-06-10 05:20:19 +02:00
commit a8acba240b
39 changed files with 7798 additions and 0 deletions

40
backend/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "gestionnaire-taches-api",
"version": "1.0.0",
"description": "API backend pour l'application de gestion de tâches.",
"author": "Ton Nom <ton.email@example.com>",
"license": "MIT",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/TonNom/TonRepo.git"
},
"scripts": {
"format": "prettier -l --write \"**/*.js\"",
"format-check": "prettier --check \"**/*.js\"",
"test": "jest",
"dev": "nodemon src/index.js"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.9.1",
"sqlite3": "^5.1.7",
"uuid": "^9.0.1",
"wait-port": "^1.1.0"
},
"resolutions": {
"@babel/core": "7.23.9"
},
"prettier": {
"trailingComma": "all",
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"prettier": "^3.2.4"
}
}

View File

@ -0,0 +1,65 @@
const db = require('../../src/persistence/sqlite');
const fs = require('fs');
const location = process.env.SQLITE_DB_LOCATION || '/etc/todos/todo.db';
const ITEM = {
id: '7aef3d7c-d301-4846-8358-2a91ec9d6be3',
name: 'Test',
completed: false,
};
beforeEach(() => {
if (fs.existsSync(location)) {
fs.unlinkSync(location);
}
});
test('it initializes correctly', async () => {
await db.init();
});
test('it can store and retrieve items', async () => {
await db.init();
await db.storeItem(ITEM);
const items = await db.getItems();
expect(items.length).toBe(1);
expect(items[0]).toEqual(ITEM);
});
test('it can update an existing item', async () => {
await db.init();
const initialItems = await db.getItems();
expect(initialItems.length).toBe(0);
await db.storeItem(ITEM);
await db.updateItem(
ITEM.id,
Object.assign({}, ITEM, { completed: !ITEM.completed }),
);
const items = await db.getItems();
expect(items.length).toBe(1);
expect(items[0].completed).toBe(!ITEM.completed);
});
test('it can remove an existing item', async () => {
await db.init();
await db.storeItem(ITEM);
await db.removeItem(ITEM.id);
const items = await db.getItems();
expect(items.length).toBe(0);
});
test('it can get a single item', async () => {
await db.init();
await db.storeItem(ITEM);
const item = await db.getItem(ITEM.id);
expect(item).toEqual(ITEM);
});

View File

@ -0,0 +1,30 @@
const db = require('../../src/persistence');
const addItem = require('../../src/routes/addItem');
const ITEM = { id: 12345 };
const { v4: uuid } = require('uuid');
jest.mock('uuid', () => ({ v4: jest.fn() }));
jest.mock('../../src/persistence', () => ({
removeItem: jest.fn(),
storeItem: jest.fn(),
getItem: jest.fn(),
}));
test('it stores item correctly', async () => {
const id = 'something-not-a-uuid';
const name = 'A sample item';
const req = { body: { name } };
const res = { send: jest.fn() };
uuid.mockReturnValue(id);
await addItem(req, res);
const expectedItem = { id, name, completed: false };
expect(db.storeItem.mock.calls.length).toBe(1);
expect(db.storeItem.mock.calls[0][0]).toEqual(expectedItem);
expect(res.send.mock.calls[0].length).toBe(1);
expect(res.send.mock.calls[0][0]).toEqual(expectedItem);
});

View File

@ -0,0 +1,20 @@
const db = require('../../src/persistence');
const deleteItem = require('../../src/routes/deleteItem');
const ITEM = { id: 12345 };
jest.mock('../../src/persistence', () => ({
removeItem: jest.fn(),
getItem: jest.fn(),
}));
test('it removes item correctly', async () => {
const req = { params: { id: 12345 } };
const res = { sendStatus: jest.fn() };
await deleteItem(req, res);
expect(db.removeItem.mock.calls.length).toBe(1);
expect(db.removeItem.mock.calls[0][0]).toBe(req.params.id);
expect(res.sendStatus.mock.calls[0].length).toBe(1);
expect(res.sendStatus.mock.calls[0][0]).toBe(200);
});

View File

@ -0,0 +1,19 @@
const db = require('../../src/persistence');
const getItems = require('../../src/routes/getItems');
const ITEMS = [{ id: 12345 }];
jest.mock('../../src/persistence', () => ({
getItems: jest.fn(),
}));
test('it gets items correctly', async () => {
const req = {};
const res = { send: jest.fn() };
db.getItems.mockReturnValue(Promise.resolve(ITEMS));
await getItems(req, res);
expect(db.getItems.mock.calls.length).toBe(1);
expect(res.send.mock.calls[0].length).toBe(1);
expect(res.send.mock.calls[0][0]).toEqual(ITEMS);
});

View File

@ -0,0 +1,33 @@
const db = require('../../src/persistence');
const updateItem = require('../../src/routes/updateItem');
const ITEM = { id: 12345 };
jest.mock('../../src/persistence', () => ({
getItem: jest.fn(),
updateItem: jest.fn(),
}));
test('it updates items correctly', async () => {
const req = {
params: { id: 1234 },
body: { name: 'New title', completed: false },
};
const res = { send: jest.fn() };
db.getItem.mockReturnValue(Promise.resolve(ITEM));
await updateItem(req, res);
expect(db.updateItem.mock.calls.length).toBe(1);
expect(db.updateItem.mock.calls[0][0]).toBe(req.params.id);
expect(db.updateItem.mock.calls[0][1]).toEqual({
name: 'New title',
completed: false,
});
expect(db.getItem.mock.calls.length).toBe(1);
expect(db.getItem.mock.calls[0][0]).toBe(req.params.id);
expect(res.send.mock.calls[0].length).toBe(1);
expect(res.send.mock.calls[0][0]).toEqual(ITEM);
});

51
backend/src/index.js Normal file
View File

@ -0,0 +1,51 @@
// Fichier: backend/src/index.js
// Point d'entrée principal pour le serveur backend.
// --- Importation des dépendances ---
const express = require('express');
const app = express();
const db = require('./persistence'); // Gestion de la base de données
const getGreeting = require('./routes/getGreeting');
const getItems = require('./routes/getItems');
const addItem = require('./routes/addItem');
const updateItem = require('./routes/updateItem');
const deleteItem = require('./routes/deleteItem');
// --- Middleware ---
// Permet au serveur de comprendre les requêtes JSON
app.use(express.json());
// Sert les fichiers statiques (si vous en avez, par exemple une version buildée du client)
app.use(express.static(__dirname + '/static'));
// --- Définition des Routes de l'API ---
// Chaque route est associée à une fonction spécifique.
app.get('/api/greeting', getGreeting); // Route pour un message d'accueil
app.get('/api/items', getItems); // Route pour obtenir toutes les tâches
app.post('/api/items', addItem); // Route pour ajouter une nouvelle tâche
app.put('/api/items/:id', updateItem); // Route pour mettre à jour une tâche existante
app.delete('/api/items/:id', deleteItem); // Route pour supprimer une tâche
// --- Démarrage du serveur ---
// Initialise la base de données, puis démarre le serveur Express.
db.init()
.then(() => {
app.listen(3000, () => console.log('Serveur démarré et à l\'écoute sur le port 3000'));
})
.catch((err) => {
console.error("Erreur lors de l'initialisation de la base de données:", err);
process.exit(1); // Quitte l'application en cas d'erreur critique
});
// --- Gestion de la fermeture propre de l'application ---
// Cette fonction s'assure que la connexion à la base de données est bien fermée
// lorsque le processus Node.js est arrêté.
const gracefulShutdown = () => {
db.teardown()
.catch(() => {})
.then(() => process.exit());
};
// Écoute les signaux d'arrêt du système
process.on('SIGINT', gracefulShutdown); // Ctrl+C
process.on('SIGTERM', gracefulShutdown); // Signal d'arrêt standard
process.on('SIGUSR2', gracefulShutdown); // Signal utilisé par nodemon pour le redémarrage

View File

@ -0,0 +1,2 @@
if (process.env.MYSQL_HOST) module.exports = require('./mysql');
else module.exports = require('./sqlite');

View File

@ -0,0 +1,108 @@
const waitPort = require('wait-port');
const fs = require('fs');
const mysql = require('mysql2');
const {
MYSQL_HOST: HOST,
MYSQL_HOST_FILE: HOST_FILE,
MYSQL_USER: USER,
MYSQL_USER_FILE: USER_FILE,
MYSQL_PASSWORD: PASSWORD,
MYSQL_PASSWORD_FILE: PASSWORD_FILE,
MYSQL_DB: DB,
MYSQL_DB_FILE: DB_FILE,
} = process.env;
let pool;
async function init() {
const host = HOST_FILE ? fs.readFileSync(HOST_FILE) : HOST;
const user = USER_FILE ? fs.readFileSync(USER_FILE) : USER;
const password = PASSWORD_FILE ? fs.readFileSync(PASSWORD_FILE) : PASSWORD;
const database = DB_FILE ? fs.readFileSync(DB_FILE) : DB;
await waitPort({
host,
port: 3306,
timeout: 10000,
waitForDns: true,
});
pool = mysql.createPool({
connectionLimit: 5,
host,
user,
password,
database,
charset: 'utf8mb4',
}).promise(); // On utilise les promesses pour un code plus propre
// MODIFIÉ : Ajout de la colonne "dueDate" de type DATE
const createTableSql = `
CREATE TABLE IF NOT EXISTS todo_items (
id varchar(36),
name varchar(255),
completed boolean,
dueDate DATE,
PRIMARY KEY (id)
) DEFAULT CHARSET utf8mb4
`;
try {
await pool.query(createTableSql);
console.log(`Connected to mysql db at host ${HOST}`);
} catch (err) {
throw new Error(err);
}
}
async function teardown() {
await pool.end();
}
async function getItems() {
const [rows] = await pool.query('SELECT * FROM todo_items');
return rows.map((item) =>
Object.assign({}, item, {
completed: item.completed === 1,
}),
);
}
async function getItem(id) {
const [rows] = await pool.query('SELECT * FROM todo_items WHERE id=?', [id]);
if (rows.length === 0) return null;
return Object.assign({}, rows[0], {
completed: rows[0].completed === 1,
});
}
async function storeItem(item) {
// MODIFIÉ : Ajout de la colonne "dueDate" dans l'insertion
await pool.query(
'INSERT INTO todo_items (id, name, completed, dueDate) VALUES (?, ?, ?, ?)',
[item.id, item.name, item.completed ? 1 : 0, item.dueDate],
);
}
async function updateItem(id, item) {
// MODIFIÉ : Ajout de "dueDate" dans la mise à jour
await pool.query(
'UPDATE todo_items SET name=?, completed=?, dueDate=? WHERE id=?',
[item.name, item.completed ? 1 : 0, item.dueDate, id],
);
}
async function removeItem(id) {
await pool.query('DELETE FROM todo_items WHERE id = ?', [id]);
}
module.exports = {
init,
teardown,
getItems,
getItem,
storeItem,
updateItem,
removeItem,
};

View File

@ -0,0 +1,124 @@
const sqlite3 = require('sqlite3').verbose();
const fs = require('fs');
const location = process.env.SQLITE_DB_LOCATION || '/etc/todos/todo.db';
let db;
function init() {
const dirName = require('path').dirname(location);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
return new Promise((acc, rej) => {
db = new sqlite3.Database(location, (err) => {
if (err) return rej(err);
if (process.env.NODE_ENV !== 'test')
console.log(`Using sqlite database at ${location}`);
// MODIFIÉ : Ajout de la colonne "dueDate" de type DATE
const createTableSql = `
CREATE TABLE IF NOT EXISTS todo_items (
id varchar(36),
name varchar(255),
completed boolean,
"dueDate" DATE
)
`;
db.run(createTableSql, (err) => {
if (err) return rej(err);
acc();
});
});
});
}
async function teardown() {
return new Promise((acc, rej) => {
db.close((err) => {
if (err) rej(err);
else acc();
});
});
}
async function getItems() {
return new Promise((acc, rej) => {
db.all('SELECT * FROM todo_items', (err, rows) => {
if (err) return rej(err);
acc(
rows.map((item) =>
Object.assign({}, item, {
completed: item.completed === 1,
}),
),
);
});
});
}
async function getItem(id) {
return new Promise((acc, rej) => {
db.all('SELECT * FROM todo_items WHERE id=?', [id], (err, rows) => {
if (err) return rej(err);
acc(
rows.map((item) =>
Object.assign({}, item, {
completed: item.completed === 1,
}),
)[0],
);
});
});
}
async function storeItem(item) {
return new Promise((acc, rej) => {
// MODIFIÉ : Ajout de la colonne "dueDate" dans l'insertion
db.run(
'INSERT INTO todo_items (id, name, completed, "dueDate") VALUES (?, ?, ?, ?)',
// MODIFIÉ : Ajout de item.dueDate aux valeurs
[item.id, item.name, item.completed ? 1 : 0, item.dueDate],
(err) => {
if (err) return rej(err);
acc();
},
);
});
}
async function updateItem(id, item) {
return new Promise((acc, rej) => {
// MODIFIÉ : Ajout de "dueDate" dans la mise à jour
db.run(
'UPDATE todo_items SET name=?, completed=?, "dueDate"=? WHERE id = ?',
// MODIFIÉ : Ajout de item.dueDate aux valeurs
[item.name, item.completed ? 1 : 0, item.dueDate, id],
(err) => {
if (err) return rej(err);
acc();
},
);
});
}
async function removeItem(id) {
return new Promise((acc, rej) => {
db.run('DELETE FROM todo_items WHERE id = ?', [id], (err) => {
if (err) return rej(err);
acc();
});
});
}
module.exports = {
init,
teardown,
getItems,
getItem,
storeItem,
updateItem,
removeItem,
};

View File

@ -0,0 +1,22 @@
const db = require('../persistence');
const { v4: uuid } = require('uuid');
module.exports = async (req, res) => {
// MODIFIÉ : On récupère 'name' et 'dueDate' du corps de la requête
const { name, dueDate } = req.body;
// Ajout d'une petite validation
if (!name || !dueDate) {
return res.status(400).send({ error: 'Le nom et la date d\'échéance sont requis.' });
}
const item = {
id: uuid(),
name: name,
completed: false,
dueDate: dueDate, // MODIFIÉ : On ajoute la date d'échéance à l'objet
};
await db.storeItem(item);
res.status(201).send(item); // On utilise 201 Created pour un ajout réussi
};

View File

@ -0,0 +1,6 @@
const db = require('../persistence');
module.exports = async (req, res) => {
await db.removeItem(req.params.id);
res.sendStatus(200);
};

View File

@ -0,0 +1,7 @@
const GREETING = 'Hello world!';
module.exports = async (req, res) => {
res.send({
greeting: GREETING,
});
};

View File

@ -0,0 +1,6 @@
const db = require('../persistence');
module.exports = async (req, res) => {
const items = await db.getItems();
res.send(items);
};

View File

@ -0,0 +1,26 @@
const db = require('../persistence');
module.exports = async (req, res) => {
// ÉTAPE 1 : Récupérer la tâche actuelle pour avoir ses valeurs par défaut
const currentItem = await db.getItem(req.params.id);
if (!currentItem) {
return res.status(404).send({ error: 'Tâche non trouvée.' });
}
// ÉTAPE 2 : Créer l'objet mis à jour.
// On utilise les nouvelles valeurs si elles existent dans la requête,
// sinon on garde les anciennes valeurs de 'currentItem'.
const updatedItem = {
name: req.body.name !== undefined ? req.body.name : currentItem.name,
completed: req.body.completed !== undefined ? req.body.completed : currentItem.completed,
dueDate: req.body.dueDate !== undefined ? req.body.dueDate : currentItem.dueDate,
};
// ÉTAPE 3 : Sauvegarder l'objet complet en base de données
await db.updateItem(req.params.id, updatedItem);
// ÉTAPE 4 : Renvoyer la tâche mise à jour au client
const item = await db.getItem(req.params.id);
res.send(item);
};

View File

3492
backend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff