mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 20:51:26 +00:00
feat: Ajout de la commande npm permettant de creer un etablissement
This commit is contained in:
437
scripts/create-establishment.js
Normal file
437
scripts/create-establishment.js
Normal file
@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* CLI pour créer un ou plusieurs établissements N3WT.
|
||||
*
|
||||
* Usage interactif :
|
||||
* node scripts/create-establishment.js
|
||||
*
|
||||
* Usage batch (fichier JSON) :
|
||||
* node scripts/create-establishment.js --file batch.json
|
||||
*
|
||||
* Format du fichier batch :
|
||||
* {
|
||||
* "backendUrl": "http://localhost:8080", // optionnel (fallback : URL_DJANGO dans conf/backend.env)
|
||||
* "apiKey": "TOk3n1234!!", // optionnel (fallback : WEBHOOK_API_KEY dans conf/backend.env)
|
||||
* "establishments": [
|
||||
* {
|
||||
* "name": "École Dupont",
|
||||
* "address": "1 rue de la Paix, Paris",
|
||||
* "total_capacity": 300,
|
||||
* "establishment_type": [1, 2], // 1=Maternelle, 2=Primaire, 3=Secondaire
|
||||
* "evaluation_frequency": 1, // 1=Trimestre, 2=Semestre, 3=Année
|
||||
* "licence_code": "LIC001", // optionnel
|
||||
* "directeur": {
|
||||
* "email": "directeur@dupont.fr",
|
||||
* "password": "motdepasse123",
|
||||
* "last_name": "Dupont",
|
||||
* "first_name": "Jean"
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
const readline = require("readline");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// ── Lecture de conf/backend.env ───────────────────────────────────────────────
|
||||
|
||||
function loadBackendEnv() {
|
||||
const envPath = path.join(__dirname, "..", "conf", "backend.env");
|
||||
if (!fs.existsSync(envPath)) return;
|
||||
|
||||
const lines = fs.readFileSync(envPath, "utf8").split("\n");
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
const eqIdx = trimmed.indexOf("=");
|
||||
if (eqIdx === -1) continue;
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
// Ne pas écraser les variables déjà définies dans l'environnement
|
||||
if (!(key in process.env)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers readline ──────────────────────────────────────────────────────────
|
||||
|
||||
let rl;
|
||||
|
||||
function getRL() {
|
||||
if (!rl) {
|
||||
rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
}
|
||||
return rl;
|
||||
}
|
||||
|
||||
function ask(question, defaultValue) {
|
||||
const suffix =
|
||||
defaultValue != null && defaultValue !== "" ? ` (${defaultValue})` : "";
|
||||
return new Promise((resolve) => {
|
||||
getRL().question(`${question}${suffix}: `, (answer) => {
|
||||
resolve(
|
||||
answer.trim() || (defaultValue != null ? String(defaultValue) : ""),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function askRequired(question) {
|
||||
return new Promise((resolve) => {
|
||||
const prompt = () => {
|
||||
getRL().question(`${question}: `, (answer) => {
|
||||
if (!answer.trim()) {
|
||||
console.log(" ⚠ Ce champ est obligatoire.");
|
||||
prompt();
|
||||
} else {
|
||||
resolve(answer.trim());
|
||||
}
|
||||
});
|
||||
};
|
||||
prompt();
|
||||
});
|
||||
}
|
||||
|
||||
function askChoices(question, choices) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${question}`);
|
||||
choices.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`));
|
||||
const prompt = () => {
|
||||
getRL().question(
|
||||
"Choix (numéros séparés par des virgules): ",
|
||||
(answer) => {
|
||||
const nums = answer
|
||||
.split(",")
|
||||
.map((s) => parseInt(s.trim(), 10))
|
||||
.filter((n) => n >= 1 && n <= choices.length);
|
||||
if (nums.length === 0) {
|
||||
console.log(" ⚠ Sélectionnez au moins une option.");
|
||||
prompt();
|
||||
} else {
|
||||
resolve(nums.map((n) => choices[n - 1].value));
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
prompt();
|
||||
});
|
||||
}
|
||||
|
||||
function askChoice(question, choices, defaultIndex) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${question}`);
|
||||
choices.forEach((c, i) =>
|
||||
console.log(
|
||||
` ${i + 1}. ${c.label}${i === defaultIndex ? " (défaut)" : ""}`,
|
||||
),
|
||||
);
|
||||
const prompt = () => {
|
||||
getRL().question(`Choix (1-${choices.length}): `, (answer) => {
|
||||
if (!answer.trim() && defaultIndex != null) {
|
||||
resolve(choices[defaultIndex].value);
|
||||
return;
|
||||
}
|
||||
const n = parseInt(answer.trim(), 10);
|
||||
if (n >= 1 && n <= choices.length) {
|
||||
resolve(choices[n - 1].value);
|
||||
} else {
|
||||
console.log(" ⚠ Choix invalide.");
|
||||
prompt();
|
||||
}
|
||||
});
|
||||
};
|
||||
prompt();
|
||||
});
|
||||
}
|
||||
|
||||
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function postJSON(url, data, apiKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const mod = parsed.protocol === "https:" ? https : http;
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
const req = mod.request(
|
||||
{
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port,
|
||||
path: parsed.pathname,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
"X-API-Key": apiKey,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let raw = "";
|
||||
res.on("data", (chunk) => (raw += chunk));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve({ status: res.statusCode, data: JSON.parse(raw) });
|
||||
} catch {
|
||||
resolve({ status: res.statusCode, data: raw });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Création d'un établissement ───────────────────────────────────────────────
|
||||
|
||||
async function createEstablishment(backendUrl, apiKey, payload) {
|
||||
const url = `${backendUrl.replace(/\/$/, "")}/Establishment/establishments`;
|
||||
const res = await postJSON(url, payload, apiKey);
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Validation basique d'un enregistrement batch ──────────────────────────────
|
||||
|
||||
function validateRecord(record, index) {
|
||||
const errors = [];
|
||||
const label = record.name ? `"${record.name}"` : `#${index + 1}`;
|
||||
|
||||
if (!record.name) errors.push("name manquant");
|
||||
if (!record.address) errors.push("address manquant");
|
||||
if (!record.total_capacity) errors.push("total_capacity manquant");
|
||||
if (
|
||||
!Array.isArray(record.establishment_type) ||
|
||||
record.establishment_type.length === 0
|
||||
)
|
||||
errors.push("establishment_type manquant ou vide");
|
||||
if (!record.directeur) errors.push("directeur manquant");
|
||||
else {
|
||||
if (!record.directeur.email) errors.push("directeur.email manquant");
|
||||
if (!record.directeur.password) errors.push("directeur.password manquant");
|
||||
if (!record.directeur.last_name)
|
||||
errors.push("directeur.last_name manquant");
|
||||
if (!record.directeur.first_name)
|
||||
errors.push("directeur.first_name manquant");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Établissement ${label} invalide : ${errors.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mode batch ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function runBatch(filePath) {
|
||||
const absPath = path.resolve(filePath);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
console.error(`❌ Fichier introuvable : ${absPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let batch;
|
||||
try {
|
||||
batch = JSON.parse(fs.readFileSync(absPath, "utf8"));
|
||||
} catch (err) {
|
||||
console.error(`❌ Fichier JSON invalide : ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const backendUrl =
|
||||
batch.backendUrl || process.env.URL_DJANGO || "http://localhost:8080";
|
||||
const apiKey = batch.apiKey || process.env.WEBHOOK_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
"❌ apiKey manquant dans le fichier batch ou la variable WEBHOOK_API_KEY.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const establishments = batch.establishments;
|
||||
if (!Array.isArray(establishments) || establishments.length === 0) {
|
||||
console.error("❌ Le fichier batch ne contient aucun établissement.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validation préalable de tous les enregistrements
|
||||
establishments.forEach((record, i) => validateRecord(record, i));
|
||||
|
||||
console.log(
|
||||
`\n📋 Batch : ${establishments.length} établissement(s) à créer sur ${backendUrl}\n`,
|
||||
);
|
||||
|
||||
let success = 0;
|
||||
let failure = 0;
|
||||
|
||||
for (let i = 0; i < establishments.length; i++) {
|
||||
const record = establishments[i];
|
||||
const label = `[${i + 1}/${establishments.length}] ${record.name}`;
|
||||
process.stdout.write(` ${label} ... `);
|
||||
|
||||
try {
|
||||
const res = await createEstablishment(backendUrl, apiKey, record);
|
||||
if (res.status === 201) {
|
||||
console.log(`✅ (ID: ${res.data.id})`);
|
||||
success++;
|
||||
} else {
|
||||
console.log(`❌ HTTP ${res.status}`);
|
||||
console.error(` ${JSON.stringify(res.data)}`);
|
||||
failure++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`❌ Erreur réseau : ${err.message}`);
|
||||
failure++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nRésultat : ${success} créé(s), ${failure} échec(s).`);
|
||||
if (failure > 0) process.exit(1);
|
||||
}
|
||||
|
||||
// ── Mode interactif ───────────────────────────────────────────────────────────
|
||||
|
||||
async function runInteractive() {
|
||||
console.log("╔══════════════════════════════════════════╗");
|
||||
console.log("║ Création d'un établissement N3WT ║");
|
||||
console.log("╚══════════════════════════════════════════╝\n");
|
||||
|
||||
const backendUrl = await ask(
|
||||
"URL du backend Django",
|
||||
process.env.URL_DJANGO || "http://localhost:8080",
|
||||
);
|
||||
const apiKey = await ask(
|
||||
"Clé API webhook (WEBHOOK_API_KEY)",
|
||||
process.env.WEBHOOK_API_KEY || "",
|
||||
);
|
||||
if (!apiKey) {
|
||||
console.error("❌ La clé API est obligatoire.");
|
||||
getRL().close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// --- Établissement ---
|
||||
console.log("\n── Établissement ──");
|
||||
const name = await askRequired("Nom de l'établissement");
|
||||
const address = await askRequired("Adresse");
|
||||
const totalCapacity = parseInt(await askRequired("Capacité totale"), 10);
|
||||
|
||||
const establishmentType = await askChoices("Type(s) de structure:", [
|
||||
{ label: "Maternelle", value: 1 },
|
||||
{ label: "Primaire", value: 2 },
|
||||
{ label: "Secondaire", value: 3 },
|
||||
]);
|
||||
|
||||
const evaluationFrequency = await askChoice(
|
||||
"Fréquence d'évaluation:",
|
||||
[
|
||||
{ label: "Trimestre", value: 1 },
|
||||
{ label: "Semestre", value: 2 },
|
||||
{ label: "Année", value: 3 },
|
||||
],
|
||||
0,
|
||||
);
|
||||
|
||||
const licenceCode = await ask("Code licence (optionnel)", "");
|
||||
|
||||
// --- Directeur (admin) ---
|
||||
console.log("\n── Compte directeur (admin) ──");
|
||||
const directorEmail = await askRequired("Email du directeur");
|
||||
const directorPassword = await askRequired("Mot de passe");
|
||||
const directorLastName = await askRequired("Nom de famille");
|
||||
const directorFirstName = await askRequired("Prénom");
|
||||
|
||||
// --- Récapitulatif ---
|
||||
console.log("\n── Récapitulatif ──");
|
||||
console.log(` Établissement : ${name}`);
|
||||
console.log(` Adresse : ${address}`);
|
||||
console.log(` Capacité : ${totalCapacity}`);
|
||||
console.log(` Type(s) : ${establishmentType.join(", ")}`);
|
||||
console.log(` Évaluation : ${evaluationFrequency}`);
|
||||
console.log(
|
||||
` Directeur : ${directorFirstName} ${directorLastName} <${directorEmail}>`,
|
||||
);
|
||||
|
||||
const confirm = await ask("\nConfirmer la création ? (o/n)", "o");
|
||||
if (confirm.toLowerCase() !== "o") {
|
||||
console.log("Annulé.");
|
||||
getRL().close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
address,
|
||||
total_capacity: totalCapacity,
|
||||
establishment_type: establishmentType,
|
||||
evaluation_frequency: evaluationFrequency,
|
||||
...(licenceCode && { licence_code: licenceCode }),
|
||||
directeur: {
|
||||
email: directorEmail,
|
||||
password: directorPassword,
|
||||
last_name: directorLastName,
|
||||
first_name: directorFirstName,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("\nCréation en cours...");
|
||||
try {
|
||||
const res = await createEstablishment(backendUrl, apiKey, payload);
|
||||
if (res.status === 201) {
|
||||
console.log("\n✅ Établissement créé avec succès !");
|
||||
console.log(` ID : ${res.data.id}`);
|
||||
console.log(` Nom : ${res.data.name}`);
|
||||
} else {
|
||||
console.error(`\n❌ Erreur (HTTP ${res.status}):`);
|
||||
console.error(JSON.stringify(res.data, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("\n❌ Erreur réseau:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
getRL().close();
|
||||
}
|
||||
|
||||
// ── Point d'entrée ────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
loadBackendEnv();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const fileIndex = args.indexOf("--file");
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
const filePath = args[fileIndex + 1];
|
||||
if (!filePath) {
|
||||
console.error(
|
||||
"❌ Argument --file manquant. Usage : --file <chemin/vers/batch.json>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await runBatch(filePath);
|
||||
} else {
|
||||
await runInteractive();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ Erreur inattendue:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user