Implement a database and an API for it (#2)
continuous-integration/drone/push Build is passing Details

Ajout de la base de données en Sqlite3 et des méthodes permettant de faire des actions sur la base de données.

Co-authored-by: hugo.pradier2 <hugo.pradier2@etu.uca.fr>
Co-authored-by: cofrizot <colin.frizot@etu.uca.fr>
Co-authored-by: clfreville2 <clement.freville2@etu.uca.fr>
Reviewed-on: #2
Reviewed-by: Clément FRÉVILLE <clement.freville2@etu.uca.fr>
Co-authored-by: Hugo PRADIER <hugo.pradier2@etu.uca.fr>
Co-committed-by: Hugo PRADIER <hugo.pradier2@etu.uca.fr>
pull/5/head
Hugo PRADIER 6 months ago committed by Clément FRÉVILLE
parent aaf24387f1
commit b58317ae88

1
.gitignore vendored

@ -8,3 +8,4 @@ package-lock.json
*.js
*.tsbuildinfo
*.d.ts
*.db

@ -7,16 +7,17 @@
"start": "tsx src/server.ts"
},
"devDependencies": {
"@types/bun": "^1.0.4",
"tsx": "^4.7.0",
"typescript": "^5.3.3",
"@types/bun": "^1.0.4"
"typescript": "^5.3.3"
},
"dependencies": {
"@fastify/cors": "^9.0.0",
"@fastify/type-provider-typebox": "^4.0.0",
"@sinclair/typebox": "^0.32.9",
"fastify": "^4.25.2",
"fastify": "^4.27.0",
"nanoid": "^5.0.4",
"sqlite3": "^5.1.7",
"zeromq": "6.0.0-beta.19"
}
}

@ -0,0 +1,328 @@
import fs from "fs";
import sqlite3 from "sqlite3";
const dbDirectory = "./src/db";
const dbFilePath = `${dbDirectory}/database.db`;
/* Fonction pour exécuter une requête sur la base de données */
/* Fonction pour exécuter une requête de modification de la base de données (INSERT, UPDATE, DELETE) */
export function runDB(
db: sqlite3.Database,
query: string,
params: any[]
): Promise<void> {
return new Promise((resolve, reject) => {
db.run(query, params, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/* Fonction pour récupérer plusieurs lignes de la base de données */
export function allDB<T>(
db: sqlite3.Database,
query: string
): Promise<unknown[]> {
return new Promise((resolve, reject) => {
db.all(query, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
/* Fonction pour récupérer une seule ligne de la base de données */
export function getDB<T>(
db: sqlite3.Database,
query: string,
params: any[]
): Promise<T[]> {
return new Promise((resolve, reject) => {
db.get(query, params, (err, row: any) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
}
/* Fonctions pour la gestion de la base de données */
/* Créer le répertoire db s'il n'existe pas */
export function createDbDirectory() {
if (!fs.existsSync(dbDirectory)) {
fs.mkdirSync(dbDirectory);
}
}
/* Ouvrir la base de données */
export function openDatabase() {
console.log("Ouverture de la connexion à la base de données.");
return new sqlite3.Database(
dbFilePath,
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
(err: Error | null) => {
if (err) console.error(err.message);
}
);
}
/* Fermer la base de données */
export function closeDatabase(db: sqlite3.Database) {
db.close((err) => {
if (err) {
console.error(err.message);
}
console.log("Fermeture de la connexion à la base de données.");
});
}
/* Create all the tables in the database */
export function createTables(db: sqlite3.Database) {
createRegisteredUserTable(db);
createLanguageTable(db);
createWorkTable(db);
}
/////////////////////////// Gestion des utilisateurs ///////////////////////////
// CREATE TABLE registered_user (
// id_user SERIAL PRIMARY KEY,
// login VARCHAR(64) NOT NULL,
// password VARCHAR(72) NOT NULL,
// permissions INT NOT NULL,
// UNIQUE (login)
// );
/* Créer la table registered_user dans la base de données */
export function createRegisteredUserTable(db: sqlite3.Database): Promise<void> {
const tableRegisteredUser = `CREATE TABLE IF NOT EXISTS registered_user (id_user INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL, password TEXT NOT NULL, permissions INTEGER NOT NULL, UNIQUE (login))`;
return runDB(db, tableRegisteredUser, []);
}
/* Insérer un utilisateur dans la table registered_user */
export function insertUser(
db: sqlite3.Database,
login: string,
password: string,
permissions: number
) {
const insertUserQuery = `INSERT INTO registered_user (login, password, permissions) VALUES (?, ?, ?)`;
return runDB(db, insertUserQuery, [login, password, permissions]);
}
/* Modifier le login d'un utilisateur dans la table registered_user */
export function updateUserLogin(
db: sqlite3.Database,
id: number,
newLogin: string
) {
const updateUserLoginQuery = `UPDATE registered_user SET login = ? WHERE id_user = ?`;
return runDB(db, updateUserLoginQuery, [newLogin, id]);
}
/* Modifier le mot de passe d'un utilisateur dans la table registered_user */
export function updateUserPassword(
db: sqlite3.Database,
id: number,
newPassword: string
) {
const updateUserPasswordQuery = `UPDATE registered_user SET password = ? WHERE id_user = ?`;
return runDB(db, updateUserPasswordQuery, [newPassword, id]);
}
/* Modifier les permissions d'un utilisateur dans la table registered_user */
export function updateUserPermissions(
db: sqlite3.Database,
id: number,
newPermissions: number
) {
const updateUserPermissionsQuery = `UPDATE registered_user SET permissions = ? WHERE id_user = ?`;
return runDB(db, updateUserPermissionsQuery, [newPermissions, id]);
}
/* Supprimer un utilisateur de la table registered_user par son ID */
export function deleteUserById(db: sqlite3.Database, id: number) {
const deleteUserQuery = `DELETE FROM registered_user WHERE id_user = ?`;
return runDB(db, deleteUserQuery, [id]);
}
/* Supprimer un utilisateur de la table registered_user par son login */
export function deleteUserByLogin(db: sqlite3.Database, login: string) {
const deleteUserQuery = `DELETE FROM registered_user WHERE login = ?`;
return runDB(db, deleteUserQuery, [login]);
}
/* Supprimer tous les utilisateurs de la table registered_user */
export function deleteAllUsers(db: sqlite3.Database) {
const deleteAllUsersQuery = `DELETE FROM registered_user`;
return runDB(db, deleteAllUsersQuery, []);
}
/* Sélectionner tous les utilisateurs de la table registered_user */
export function selectAllUsers(db: sqlite3.Database): Promise<unknown[]> {
const selectAllUsersQuery = `SELECT * FROM registered_user`;
return allDB(db, selectAllUsersQuery);
}
/* Sélectionner un utilisateur par son login */
export function selectUserByLogin(db: sqlite3.Database, login: string) {
const selectUserByLoginQuery = `SELECT * FROM registered_user WHERE login = ?`;
return getDB(db, selectUserByLoginQuery, [login]);
}
/* Sélectionner un utilisateur par son ID */
export function selectUserById(db: sqlite3.Database, id: number) {
const selectUserByIdQuery = `SELECT * FROM registered_user WHERE id_user = ?`;
return getDB(db, selectUserByIdQuery, [id]);
}
/////////////////////////// Gestion des Languages ///////////////////////////
// CREATE TABLE language (
// id_language SERIAL PRIMARY KEY,
// designation VARCHAR(30) NOT NULL,
// version INT NOT NULL,
// );
/* Créer la table language dans la base de données */
export function createLanguageTable(db: sqlite3.Database): Promise<void> {
const tableLanguage = `CREATE TABLE IF NOT EXISTS language (id_language INTEGER PRIMARY KEY AUTOINCREMENT, designation TEXT NOT NULL, version INTEGER NOT NULL)`;
return runDB(db, tableLanguage, []);
}
/* Insérer un language dans la table language */
export function insertLanguage(
db: sqlite3.Database,
designation: string,
version: number
) {
const insertLanguageQuery = `INSERT INTO language (designation, version) VALUES (?, ?)`;
return runDB(db, insertLanguageQuery, [designation, version]);
}
/* Modifier la designation d'un language dans la table language */
export function updateLanguageDesignation(
db: sqlite3.Database,
id: number,
newDesignation: string
) {
const updateLanguageDesignationQuery = `UPDATE language SET designation = ? WHERE id_language = ?`;
return runDB(db, updateLanguageDesignationQuery, [newDesignation, id]);
}
/* Modifier la version d'un language dans la table language */
export function updateLanguageVersion(
db: sqlite3.Database,
id: number,
newVersion: number
) {
const updateLanguageVersionQuery = `UPDATE language SET version = ? WHERE id_language = ?`;
return runDB(db, updateLanguageVersionQuery, [newVersion, id]);
}
/* Supprimer un language de la table language par son ID */
export function deleteLanguage(db: sqlite3.Database, id: number) {
const deleteLanguageQuery = `DELETE FROM language WHERE id_language = ?`;
return runDB(db, deleteLanguageQuery, [id]);
}
/* Supprimer tous les languages de la table language */
export function deleteAllLanguages(db: sqlite3.Database) {
const deleteAllLanguagesQuery = `DELETE FROM language`;
return runDB(db, deleteAllLanguagesQuery, []);
}
/* Sélectionner tous les languages de la table language */
export function selectAllLanguages(db: sqlite3.Database): Promise<unknown[]> {
const selectAllLanguagesQuery = `SELECT * FROM language`;
return allDB(db, selectAllLanguagesQuery);
}
/* Sélectionner un language par son ID */
export function selectLanguageById(db: sqlite3.Database, id: number) {
const selectLanguageByIdQuery = `SELECT * FROM language WHERE id_language = ?`;
return getDB(db, selectLanguageByIdQuery, [id]);
}
/////////////////////////// Gestion des works ///////////////////////////
// CREATE TABLE work (
// id_work SERIAL PRIMARY KEY,
// link CHAR(36) NOT NULL,
// user_id INT REFERENCES registered_user(id_user),
// language_id INT NOT NULL REFERENCES language(id_language)
// content TEXT NOT NULL,
// );
/* Créer la table work dans la base de données */
export function createWorkTable(db: sqlite3.Database): Promise<void> {
const tableWork = `CREATE TABLE IF NOT EXISTS work (id_work INTEGER PRIMARY KEY AUTOINCREMENT, link CHAR(36) NOT NULL, user_id INTEGER REFERENCES registered_user(id_user), language_id INTEGER NOT NULL REFERENCES language(id_language), content TEXT NOT NULL)`;
return runDB(db, tableWork, []);
}
/* Insérer un work dans la table work */
export function insertWork(
db: sqlite3.Database,
link: string,
user_id: number,
language_id: number,
content: string
) {
const insertWorkQuery = `INSERT INTO work (link, user_id, language_id, content) VALUES (?, ?, ?, ?)`;
return runDB(db, insertWorkQuery, [link, user_id, language_id, content]);
}
/* Sélectionner tous les works de la table work */
export function selectAllWorks(db: sqlite3.Database): Promise<unknown[]> {
const selectAllWorksQuery = `SELECT * FROM work`;
return allDB(db, selectAllWorksQuery);
}
/* Supprimer tous les works de la table work */
export function deleteAllWorks(db: sqlite3.Database) {
const deleteAllWorksQuery = `DELETE FROM work`;
return runDB(db, deleteAllWorksQuery, []);
}
/* Supprimer un work de la table work */
export function deleteWork(db: sqlite3.Database, id: number) {
const deleteWorkQuery = `DELETE FROM work WHERE id_work = ?`;
return runDB(db, deleteWorkQuery, [id]);
}
/* Sélectionner un work par son ID */
export function selectWorkById(db: sqlite3.Database, id: number) {
const selectWorkByIdQuery = `SELECT * FROM work WHERE id_work = ?`;
return getDB(db, selectWorkByIdQuery, [id]);
}

@ -1,8 +1,12 @@
export const RUNNERS = ['bash', 'moshell', 'bun', 'typescript'] as const;
export const RUNNERS = ["bash", "moshell", "bun", "typescript"] as const;
const ALLOWED_LANGUAGES = new Set<string>(RUNNERS);
const aliases: Record<string, typeof RUNNERS[number]> = {
'JavaScript': 'bun',
'TypeScript': 'typescript',
const aliases: Record<string, (typeof RUNNERS)[number]> = {
JavaScript: "bun",
TypeScript: "typescript",
};
export const IMAGES = {
logo: "logo.png",
background: "background.png",
};
/**
@ -12,9 +16,15 @@ const aliases: Record<string, typeof RUNNERS[number]> = {
* @param code The code to be executed.
* @param image The image to be used.
*/
export function allocateBuffer(jobId: string, code: string, image: string): Buffer {
export function allocateBuffer(
jobId: string,
code: string,
image: string
): Buffer {
let cur = 0;
const buffer = Buffer.allocUnsafe(jobId.length + image.length + code.length + 9);
const buffer = Buffer.allocUnsafe(
jobId.length + image.length + code.length + 9
);
cur = buffer.writeUInt8(0, cur);
cur += buffer.write(jobId, cur);
cur = buffer.writeUInt32BE(image.length, cur);
@ -24,10 +34,10 @@ export function allocateBuffer(jobId: string, code: string, image: string): Buff
return buffer;
}
export function getRunner(language: string): typeof RUNNERS[number] | null {
export function getRunner(language: string): (typeof RUNNERS)[number] | null {
language = aliases[language] || language;
if (ALLOWED_LANGUAGES.has(language)) {
return language as typeof RUNNERS[number];
return language as (typeof RUNNERS)[number];
}
return null;
}

@ -1,9 +1,13 @@
import cors from '@fastify/cors';
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import Fastify, { FastifyReply } from 'fastify';
import { nanoid } from 'nanoid';
import { allocateBuffer, getRunner } from 'runner';
import { Pull, Push } from 'zeromq';
import cors from "@fastify/cors";
import { Type, TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import Fastify, { FastifyReply, FastifyRequest } from "fastify";
import { nanoid } from "nanoid";
import { allocateBuffer, IMAGES } from "runner";
import { Pull, Push } from "zeromq";
import * as db from "./database";
console.log(IMAGES.logo);
const sender = new Push();
await sender.bind(`tcp://127.0.0.1:5557`);
@ -17,41 +21,377 @@ const fastify = Fastify({
logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();
await fastify.register(cors, {
origin: process.env.ALLOW_ORIGIN || '*',
origin: process.env.ALLOW_ORIGIN || "*",
});
/* Database */
/* Création du répertoire de la base de données s'il n'existe pas */
db.createDbDirectory();
/* Ouvrir la base de données */
const database = db.openDatabase();
/* Créer les tables si elles n'existent pas */
db.createTables(database);
/* Route pour créer un utilisateur */
fastify.post(
"/users",
{
schema: {
body: Type.Object({
login: Type.String(),
password: Type.String(),
permissions: Type.Number(),
}),
},
},
async (request, reply) => {
const { login, password, permissions } = request.body;
db.insertUser(database, login, password, permissions);
reply.send({ success: true });
}
);
/* Route pour mettre à jour le login d'un utilisateur */
fastify.put(
"/users/:id/login",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newLogin: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newLogin } = request.body;
db.updateUserLogin(database, id, newLogin);
reply.send({ success: true });
}
);
/* Route pour mettre à jour le mot de passe d'un utilisateur */
fastify.put(
"/users/:id/password",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newPassword: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newPassword } = request.body;
db.updateUserPassword(database, id, newPassword);
reply.send({ success: true });
}
);
/* Route pour mettre à jour les permissions d'un utilisateur */
fastify.put(
"/users/:id/permissions",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newPermissions: Type.Number(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newPermissions } = request.body;
await db.updateUserPermissions(database, id, newPermissions);
reply.send({ success: true });
}
);
/* Route pour supprimer un utilisateur par son ID */
fastify.delete(
"/users/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
await db.deleteUserById(database, id);
reply.send({ success: true });
}
);
/* Route pour supprimer un utilisateur par son login */
fastify.delete(
"/users/login/:login",
{
schema: {
params: Type.Object({
login: Type.String(),
}),
},
},
async (request, reply) => {
const { login } = request.params;
await db.deleteUserByLogin(database, login);
reply.send({ success: true });
}
);
/* Route pour supprimer tous les utilisateurs */
fastify.delete("/users", async (request, reply) => {
await db.deleteAllUsers(database);
reply.send({ success: true });
});
/* Route pour récupérer tous les utilisateurs */
fastify.get("/users", async (request, reply) => {
const users = await db.selectAllUsers(database);
reply.send(users);
});
/* Route pour récupérer un utilisateur par son ID */
fastify.get(
"/users/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const user = await db.selectUserById(database, id);
reply.send(user);
}
);
/* Route pour récupérer un utilisateur par son login */
fastify.get(
"/users/login/:login",
{
schema: {
params: Type.Object({
login: Type.String(),
}),
},
},
async (request, reply) => {
const { login } = request.params;
const user = await db.selectUserByLogin(database, login);
reply.send(user);
}
);
/* Route pour créer un language */
fastify.post(
"/languages",
{
schema: {
body: Type.Object({
designation: Type.String(),
version: Type.Number(),
}),
},
},
async (request, reply) => {
const { designation, version } = request.body;
db.insertLanguage(database, designation, version);
reply.send({ success: true });
}
);
/* Route pour mettre à jour la désignation d'un language */
fastify.put(
"/languages/:id/designation",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newDesignation: Type.String(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newDesignation } = request.body;
db.updateLanguageDesignation(database, id, newDesignation);
reply.send({ success: true });
}
);
/* Route pour mettre à jour la version d'un language */
fastify.put(
"/languages/:id/version",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
body: Type.Object({
newVersion: Type.Number(),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const { newVersion } = request.body;
db.updateLanguageVersion(database, id, newVersion);
reply.send({ success: true });
}
);
/* Route pour supprimer un language */
fastify.delete(
"/languages/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
db.deleteLanguage(database, id);
reply.send({ success: true });
}
);
/* Route pour supprimer tous les languages */
fastify.delete("/languages", async (request, reply) => {
db.deleteAllLanguages(database);
reply.send({ success: true });
});
fastify.post('/run', {
schema: {
body: Type.Object({
code: Type.String(),
language: Type.String(),
}),
},
}, (req, reply) => {
const { code, language } = req.body;
const runner = getRunner(language);
if (runner === null) {
return reply.status(422).send({ error: 'Invalid language' });
}
const jobId = generateId();
const buffer = allocateBuffer(jobId, code, runner);
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Access-Control-Allow-Origin': process.env.ALLOW_ORIGIN || '*',
});
reply.raw.on('close', () => {
delete clients[jobId];
});
sender.send(buffer).then(() => {
reply.raw.write('event: connected\n');
reply.raw.write(`data: ${jobId}\n`);
reply.raw.write('id: 0\n\n');
});
clients[jobId] = reply;
/* Route pour récupérer un language par son ID */
fastify.get(
"/languages/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const language = await db.selectLanguageById(database, id);
reply.send(language);
}
);
/* Route pour récupérer tous les languages */
fastify.get("/languages", async (request, reply) => {
const languages = await db.selectAllLanguages(database);
reply.send(languages);
});
/* Route pour créer un work */
fastify.post(
"/works",
{
schema: {
body: Type.Object({
id_user: Type.Number(),
link: Type.String(),
id_language: Type.Number(),
code: Type.String(),
}),
},
},
async (request, reply) => {
const { id_user, link, id_language, code } = request.body;
db.insertWork(database, link, id_user, id_language, code);
reply.send({ success: true });
}
);
/* Route pour récupérer tous les works */
fastify.get("/works", async (request, reply) => {
const works = await db.selectAllWorks(database);
reply.send(works);
});
/* Route pour supprimer tous les works */
fastify.delete("/works", async (request, reply) => {
db.deleteAllWorks(database);
reply.send({ success: true });
});
/* Route pour supprimer un work par son ID */
fastify.delete(
"/works/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
db.deleteWork(database, id);
reply.send({ success: true });
}
);
/* Route pour récupérer un work par son ID */
fastify.get(
"/works/:id",
{
schema: {
params: Type.Object({
id: Type.Number({
minimum: 0,
}),
}),
},
},
async (request, reply) => {
const { id } = request.params;
const work = await db.selectWorkById(database, id);
reply.send(work);
}
);
/* Forward output est une fonction asynchrone qui permet de récupérer les messages envoyés par le container et de les renvoyer au client */
async function forwardOutput() {
for await (const [buff] of receiver) {
const messageType = buff.readInt8();
@ -66,25 +406,26 @@ async function forwardOutput() {
case 1:
case 2:
const text = encodeURIComponent(buff.subarray(33).toString());
raw.write('event: message\n');
raw.write("event: message\n");
if (messageType === 1) {
raw.write('id: stdout\n');
raw.write("id: stdout\n");
} else {
raw.write('id: stderr\n');
raw.write("id: stderr\n");
}
raw.write(`data: ${text}\n\n`);
break;
case 3:
const exitCode = buff.readUint32BE(33);
raw.write('event: message\n');
raw.write('id: exit\n');
raw.write("event: message\n");
raw.write("id: exit\n");
raw.write(`data: ${exitCode}\n\n`);
raw.end();
break;
default:
console.error('Unknown message type', messageType);
console.error("Unknown message type", messageType);
}
}
}
/* Lancer le serveur et la fonction forwardOutput sur le même thread en parallèle */
await Promise.all([fastify.listen({ port: 3000 }), forwardOutput()]);

Loading…
Cancel
Save