Step 4 migration: enable API/DB write operations and frontend CRUD
This commit is contained in:
245
api/server.js
245
api/server.js
@@ -16,6 +16,68 @@ function sendJson(response, statusCode, payload) {
|
||||
response.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
async function readJsonBody(request) {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
|
||||
for await (const chunk of request) {
|
||||
size += chunk.length;
|
||||
if (size > 1024 * 1024) {
|
||||
throw new Error("Payload too large");
|
||||
}
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
if (!chunks.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
async function ensureConsole(client, brandNameRaw, consoleNameRaw) {
|
||||
const brandName = normalizeText(brandNameRaw).toUpperCase();
|
||||
const consoleName = normalizeText(consoleNameRaw);
|
||||
|
||||
if (!brandName || !consoleName) {
|
||||
throw new Error("brand and consoleName are required");
|
||||
}
|
||||
|
||||
const brandResult = await client.query(
|
||||
`
|
||||
INSERT INTO brands(name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id;
|
||||
`,
|
||||
[brandName],
|
||||
);
|
||||
|
||||
const brandId = brandResult.rows[0].id;
|
||||
const consoleResult = await client.query(
|
||||
`
|
||||
INSERT INTO consoles(brand_id, name)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (brand_id, name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id;
|
||||
`,
|
||||
[brandId, consoleName],
|
||||
);
|
||||
|
||||
return {
|
||||
brand: brandName,
|
||||
consoleName,
|
||||
consoleId: consoleResult.rows[0].id,
|
||||
};
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
await pool.query("CREATE EXTENSION IF NOT EXISTS pgcrypto;");
|
||||
await pool.query(`
|
||||
@@ -176,6 +238,125 @@ async function getCatalogFull() {
|
||||
return { brands, gamesByConsole };
|
||||
}
|
||||
|
||||
async function createConsole(brand, consoleName) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const result = await ensureConsole(client, brand, consoleName);
|
||||
await client.query("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function createGame(payload) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const consoleData = await ensureConsole(client, payload.brand, payload.consoleName);
|
||||
|
||||
const title = normalizeText(payload.title);
|
||||
if (!title) {
|
||||
throw new Error("title is required");
|
||||
}
|
||||
|
||||
const genre = normalizeText(payload.genre) || null;
|
||||
const publisher = normalizeText(payload.publisher) || null;
|
||||
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null;
|
||||
const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null;
|
||||
|
||||
const insertResult = await client.query(
|
||||
`
|
||||
INSERT INTO games(
|
||||
console_id, title, genre, publisher, release_year, estimated_value, loaned_to
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id::text AS id;
|
||||
`,
|
||||
[consoleData.consoleId, title, genre, publisher, year, value, loanedTo],
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { id: insertResult.rows[0].id };
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGame(id, payload) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const consoleData = await ensureConsole(client, payload.brand, payload.consoleName);
|
||||
|
||||
const title = normalizeText(payload.title);
|
||||
if (!title) {
|
||||
throw new Error("title is required");
|
||||
}
|
||||
|
||||
const genre = normalizeText(payload.genre) || null;
|
||||
const publisher = normalizeText(payload.publisher) || null;
|
||||
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null;
|
||||
const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null;
|
||||
|
||||
const updateResult = await client.query(
|
||||
`
|
||||
UPDATE games
|
||||
SET
|
||||
console_id = $2,
|
||||
title = $3,
|
||||
genre = $4,
|
||||
publisher = $5,
|
||||
release_year = $6,
|
||||
estimated_value = $7,
|
||||
loaned_to = $8
|
||||
WHERE id = $1::uuid
|
||||
RETURNING id::text AS id;
|
||||
`,
|
||||
[id, consoleData.consoleId, title, genre, publisher, year, value, loanedTo],
|
||||
);
|
||||
|
||||
if (!updateResult.rowCount) {
|
||||
throw new Error("game not found");
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return { id: updateResult.rows[0].id };
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGame(id) {
|
||||
const result = await pool.query("DELETE FROM games WHERE id = $1::uuid;", [id]);
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
async function toggleGameLoan(id) {
|
||||
const result = await pool.query(
|
||||
`
|
||||
UPDATE games
|
||||
SET loaned_to = CASE WHEN COALESCE(loaned_to, '') = '' THEN 'A renseigner' ELSE NULL END
|
||||
WHERE id = $1::uuid
|
||||
RETURNING id::text AS id;
|
||||
`,
|
||||
[id],
|
||||
);
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
async function handleRequest(request, response) {
|
||||
const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
|
||||
|
||||
@@ -229,6 +410,70 @@ async function handleRequest(request, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/api/catalog/consoles") {
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
const created = await createConsole(body.brand, body.consoleName);
|
||||
sendJson(response, 201, created);
|
||||
} catch (error) {
|
||||
sendJson(response, 400, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url.pathname === "/api/catalog/games") {
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
const created = await createGame(body);
|
||||
sendJson(response, 201, created);
|
||||
} catch (error) {
|
||||
sendJson(response, 400, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const gameIdMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)$/);
|
||||
if (request.method === "PUT" && gameIdMatch) {
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
const updated = await updateGame(gameIdMatch[1], body);
|
||||
sendJson(response, 200, updated);
|
||||
} catch (error) {
|
||||
const statusCode = error.message === "game not found" ? 404 : 400;
|
||||
sendJson(response, statusCode, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "DELETE" && gameIdMatch) {
|
||||
try {
|
||||
const deleted = await deleteGame(gameIdMatch[1]);
|
||||
if (!deleted) {
|
||||
sendJson(response, 404, { status: "error", message: "game not found" });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, { status: "ok" });
|
||||
} catch (error) {
|
||||
sendJson(response, 400, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const gameToggleMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)\/toggle-loan$/);
|
||||
if (request.method === "POST" && gameToggleMatch) {
|
||||
try {
|
||||
const toggled = await toggleGameLoan(gameToggleMatch[1]);
|
||||
if (!toggled) {
|
||||
sendJson(response, 404, { status: "error", message: "game not found" });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 200, { status: "ok" });
|
||||
} catch (error) {
|
||||
sendJson(response, 400, { status: "error", message: error.message });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(response, 404, {
|
||||
status: "not_found",
|
||||
message: "Route not found",
|
||||
|
||||
Reference in New Issue
Block a user