-
Notifications
You must be signed in to change notification settings - Fork 0
Implement persistent storage for todos and enhance logging: Added fil… #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # dependencies | ||
| node_modules/ | ||
|
|
||
| # Bun lockfile | ||
| bun.lockb | ||
|
|
||
| # data files | ||
| data/ | ||
|
|
||
| # macOS | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { todoStore, type CreateTodoInput } from "./todoStore"; | ||
| import { todoStore, type CreateTodoInput, initTodoStore } from "./todoStore"; | ||
|
|
||
| const corsHeaders = { | ||
| "Access-Control-Allow-Origin": "*", | ||
|
|
@@ -41,17 +41,27 @@ const notFound = () => json({ error: "Not Found" }, { status: 404 }); | |
| const noContent = (status = 204) => | ||
| new Response(null, { status, headers: corsHeaders }); | ||
|
|
||
| const safeJsonBody = async (req: Request) => { | ||
| const safeJsonBody = async ( | ||
| req: Request, | ||
| opts: { maxBytes?: number } = {}, | ||
| ) => { | ||
| try { | ||
| return { | ||
| ok: true as const, | ||
| value: (await req.json()) as unknown, | ||
| }; | ||
| const max = opts.maxBytes ?? 1_000_000; // 1MB default | ||
| const lenHeader = req.headers.get("content-length"); | ||
| if (lenHeader) { | ||
| const len = Number.parseInt(lenHeader, 10); | ||
| if (Number.isFinite(len) && len > max) { | ||
| return { ok: false as const, error: "Payload Too Large", status: 413 }; | ||
| } | ||
| } | ||
| const textBody = await req.text(); | ||
| const bytes = new TextEncoder().encode(textBody).length; | ||
| if (bytes > max) { | ||
| return { ok: false as const, error: "Payload Too Large", status: 413 }; | ||
| } | ||
| return { ok: true as const, value: JSON.parse(textBody) as unknown }; | ||
| } catch { | ||
| return { | ||
| ok: false as const, | ||
| error: "Invalid JSON body", | ||
| }; | ||
| return { ok: false as const, error: "Invalid JSON body", status: 400 }; | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -134,120 +144,200 @@ const parseUpdateInput = (payload: unknown) => { | |
| return { ok: true as const, value: update }; | ||
| }; | ||
|
|
||
| await initTodoStore(); | ||
|
|
||
| const server = Bun.serve({ | ||
| port: Number(process.env.PORT ?? 3000), | ||
| async fetch(req) { | ||
| const start = Date.now(); | ||
| const requestId = crypto.randomUUID(); | ||
| const url = new URL(req.url); | ||
|
|
||
| if (req.method === "OPTIONS") { | ||
| return new Response(null, { status: 204, headers: corsHeaders }); | ||
| } | ||
|
|
||
| if (req.method === "GET" && url.pathname === "/health") { | ||
| return text("ok"); | ||
| } | ||
|
|
||
| const todosMatch = todosPattern.exec(req.url); | ||
| if (todosMatch) { | ||
| if (req.method === "GET") { | ||
| const completedParam = url.searchParams.get("completed"); | ||
| if ( | ||
| completedParam !== null && | ||
| completedParam !== "true" && | ||
| completedParam !== "false" | ||
| ) { | ||
| return json( | ||
| { | ||
| error: "Parameter 'completed' must be either 'true' or 'false'", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
| const handle = async () => { | ||
| if (req.method === "OPTIONS") { | ||
| return new Response(null, { status: 204, headers: corsHeaders }); | ||
| } | ||
|
|
||
| const filter = | ||
| completedParam === null | ||
| ? {} | ||
| : { completed: completedParam === "true" }; | ||
| if (req.method === "GET" && url.pathname === "/") { | ||
| return text("Todo API (Bun) — veja /health e /todos"); | ||
| } | ||
|
|
||
| return json(todoStore.list(filter)); | ||
| if (req.method === "GET" && url.pathname === "/health") { | ||
| return text("ok"); | ||
| } | ||
|
|
||
| if (req.method === "POST") { | ||
| if (!ensureJson(req)) { | ||
| return json( | ||
| { error: "Content-Type must be application/json" }, | ||
| { status: 415 }, | ||
| const todosMatch = todosPattern.exec(req.url); | ||
| if (todosMatch) { | ||
| if (req.method === "GET") { | ||
| const completedParam = url.searchParams.get("completed"); | ||
| const limitParam = url.searchParams.get("limit"); | ||
| const offsetParam = url.searchParams.get("offset"); | ||
| const sortBy = url.searchParams.get("sort") ?? undefined; | ||
| const order = url.searchParams.get("order") ?? undefined; | ||
| if ( | ||
| completedParam !== null && | ||
| completedParam !== "true" && | ||
| completedParam !== "false" | ||
| ) { | ||
| return json( | ||
| { | ||
| error: "Parameter 'completed' must be either 'true' or 'false'", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const filter = | ||
| completedParam === null | ||
| ? {} | ||
| : { completed: completedParam === "true" }; | ||
|
|
||
| const maxLimit = 1000; | ||
| const limit = Math.min( | ||
| maxLimit, | ||
| Math.max(0, Number.parseInt(limitParam ?? "50", 10) || 50), | ||
| ); | ||
| const offset = Math.max(0, Number.parseInt(offsetParam ?? "0", 10) || 0); | ||
| const validSort = new Set(["id", "createdAt", "updatedAt", "title"]); | ||
| const validOrder = new Set(["asc", "desc"]); | ||
| const sortOpt = validSort.has(String(sortBy)) ? (sortBy as any) : "id"; | ||
| const orderOpt = validOrder.has(String(order)) ? (order as any) : "asc"; | ||
|
|
||
| const { items, total } = todoStore.list(filter, { | ||
| limit, | ||
| offset, | ||
| sortBy: sortOpt, | ||
| order: orderOpt, | ||
| }); | ||
|
|
||
| return json(items, { headers: { "X-Total-Count": String(total) } }); | ||
| } | ||
|
|
||
| const parsed = await safeJsonBody(req); | ||
| if (!parsed.ok) { | ||
| return json({ error: parsed.error }, { status: 400 }); | ||
| if (req.method === "POST") { | ||
| if (!ensureJson(req)) { | ||
| return json( | ||
| { error: "Content-Type must be application/json" }, | ||
| { status: 415 }, | ||
| ); | ||
| } | ||
|
|
||
| const parsed = await safeJsonBody(req); | ||
| if (!parsed.ok) { | ||
| return json({ error: parsed.error }, { status: parsed.status ?? 400 }); | ||
| } | ||
|
|
||
| const validated = parseCreateInput(parsed.value); | ||
| if (!validated.ok) { | ||
| return json({ error: validated.error }, { status: 400 }); | ||
| } | ||
|
|
||
| const todo = todoStore.create(validated.value); | ||
| return json(todo, { status: 201 }); | ||
| } | ||
|
|
||
| const validated = parseCreateInput(parsed.value); | ||
| if (!validated.ok) { | ||
| return json({ error: validated.error }, { status: 400 }); | ||
| if (req.method === "DELETE") { | ||
| const completedParam = url.searchParams.get("completed"); | ||
| if ( | ||
| completedParam !== "true" && | ||
| completedParam !== "false" | ||
| ) { | ||
| return json( | ||
| { error: "Parameter 'completed' must be 'true' or 'false'" }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
| const removed = todoStore.deleteMany({ | ||
| completed: completedParam === "true", | ||
| }); | ||
| return json({ removed }); | ||
| } | ||
|
|
||
| const todo = todoStore.create(validated.value); | ||
| return json(todo, { status: 201 }); | ||
| return methodNotAllowed(); | ||
| } | ||
|
|
||
| return methodNotAllowed(); | ||
| } | ||
|
|
||
| const todoMatch = todoIdPattern.exec(req.url); | ||
| if (todoMatch) { | ||
| const idParam = todoMatch.pathname.groups.id; | ||
| const id = Number.parseInt(idParam ?? "", 10); | ||
| if (!Number.isFinite(id)) { | ||
| return json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| const existing = todoStore.get(id); | ||
| if (!existing) { | ||
| return notFound(); | ||
| } | ||
| const todoMatch = todoIdPattern.exec(req.url); | ||
| if (todoMatch) { | ||
| const idParam = todoMatch.pathname.groups.id; | ||
| const id = Number.parseInt(idParam ?? "", 10); | ||
| if (!Number.isFinite(id)) { | ||
| return json({ error: "Invalid id" }, { status: 400 }); | ||
| } | ||
|
|
||
| if (req.method === "GET") { | ||
| return json(existing); | ||
| } | ||
| const existing = todoStore.get(id); | ||
| if (!existing) { | ||
| return notFound(); | ||
| } | ||
|
|
||
| if (req.method === "PATCH") { | ||
| if (!ensureJson(req)) { | ||
| return json( | ||
| { error: "Content-Type must be application/json" }, | ||
| { status: 415 }, | ||
| ); | ||
| if (req.method === "GET") { | ||
| return json(existing); | ||
| } | ||
|
|
||
| const parsed = await safeJsonBody(req); | ||
| if (!parsed.ok) { | ||
| return json({ error: parsed.error }, { status: 400 }); | ||
| if (req.method === "PATCH") { | ||
| if (!ensureJson(req)) { | ||
| return json( | ||
| { error: "Content-Type must be application/json" }, | ||
| { status: 415 }, | ||
| ); | ||
| } | ||
|
|
||
| const parsed = await safeJsonBody(req); | ||
| if (!parsed.ok) { | ||
| return json({ error: parsed.error }, { status: parsed.status ?? 400 }); | ||
| } | ||
|
|
||
| const validated = parseUpdateInput(parsed.value); | ||
| if (!validated.ok) { | ||
| return json({ error: validated.error }, { status: 400 }); | ||
| } | ||
|
|
||
| const updated = todoStore.update(id, validated.value); | ||
| return json(updated); | ||
| } | ||
|
|
||
| const validated = parseUpdateInput(parsed.value); | ||
| if (!validated.ok) { | ||
| return json({ error: validated.error }, { status: 400 }); | ||
| if (req.method === "DELETE") { | ||
| todoStore.delete(id); | ||
| return noContent(); | ||
| } | ||
|
|
||
| const updated = todoStore.update(id, validated.value); | ||
| return json(updated); | ||
| return methodNotAllowed(); | ||
| } | ||
|
|
||
| if (req.method === "DELETE") { | ||
| todoStore.delete(id); | ||
| return noContent(); | ||
| } | ||
| return notFound(); | ||
| }; | ||
|
|
||
| return methodNotAllowed(); | ||
| try { | ||
| const res = await handle(); | ||
| const duration = Date.now() - start; | ||
| const headers = new Headers(res.headers); | ||
| headers.set("X-Request-Id", requestId); | ||
| console.log( | ||
| `${requestId} ${req.method} ${url.pathname} -> ${res.status} ${duration}ms`, | ||
| ); | ||
| return new Response(res.body, { status: res.status, headers }); | ||
| } catch (err) { | ||
| console.error("Erro inesperado:", err); | ||
| const duration = Date.now() - start; | ||
| console.log(`${requestId} ${req.method} ${url.pathname} -> 500 ${duration}ms`); | ||
| return json( | ||
| { error: "Internal Server Error" }, | ||
| { status: 500, headers: { "X-Request-Id": requestId } }, | ||
| ); | ||
| } | ||
|
|
||
| return notFound(); | ||
| }, | ||
| }); | ||
|
|
||
| console.log( | ||
| `Todo API ouvindo em http://localhost:${server.port} (pid ${process.pid})`, | ||
| ); | ||
|
|
||
| // Encerramento gracioso | ||
| const shutdown = (signal: string) => { | ||
| console.log(`Recebido ${signal}, finalizando...`); | ||
| try { | ||
| server.stop(); | ||
| } catch {} | ||
| process.exit(0); | ||
| }; | ||
|
Comment on lines
+334
to
+340
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const shutdown = (signal: string) => {
console.log(`Received ${signal}, shutting down...`);
server.stop(true);
};The current shutdown implementation uses To ensure data integrity, the server should shut down gracefully. By using Talk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| process.on("SIGINT", () => shutdown("SIGINT")); | ||
| process.on("SIGTERM", () => shutdown("SIGTERM")); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The project's contribution guidelines require using
npm startordocker composefor running the application. The commandbun run --watch src/server.tsdoes not follow these standards. Please update the documentation to reflect the standard commands for development to ensure a consistent workflow.Kody Rule violation: Use npm start ou docker-start, nunca npm run build
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.