příklady / ukládání dat

Návštěvní kniha

Formulář návštěvní knihy nad libsql, kde je každý záznam bezpečně ošetřen.

libsqlformsstorage

Návštěvníci napíšou jméno a zprávu a stránka vypíše všechny, kdo se podepsali. Nasazením tohoto příkladu vznikne libsql databáze, která se do služby propojí jako GUESTBOOK_DB, connection string bez tokenu; přihlašovací údaje vloží platforma až na hraně sítě, takže v kódu není žádné databázové heslo.

Služba si tabulku vytvoří na vyžádání pomocí CREATE TABLE IF NOT EXISTS, každý záznam ukládá parametrizovaným dotazem (vstup uživatele se nikdy nepovažuje za SQL) a před vykreslením ošetří každé jméno i zprávu (zpráva tak nemůže vložit HTML ani skript). Po úspěšném POSTu přesměruje, takže obnovení stránky formulář neodešle znovu.

kód
main.ts
// A classic guestbook: visitors leave a name and a message, and the page shows
// everyone who signed. Data lives in a libsql database that was created and
// linked for you at deploy time. Its connection string arrives in GUESTBOOK_DB
// as a tokenless URL; the platform injects the credentials at the network edge,
// so there is never a database secret in your code.

import { createClient } from "npm:@libsql/client@0.14.0/web";

const db = createClient({ url: Deno.env.get("GUESTBOOK_DB")! });

type Entry = { name: string; message: string; created_at: string };

export default async (req: Request): Promise<Response> => {
  // Create the table on demand. IF NOT EXISTS makes this safe to run every time.
  await db.execute(
    `CREATE TABLE IF NOT EXISTS entries (
       id INTEGER PRIMARY KEY AUTOINCREMENT,
       name TEXT NOT NULL,
       message TEXT NOT NULL,
       created_at TEXT NOT NULL DEFAULT (datetime('now'))
     )`,
  );

  const { pathname } = new URL(req.url);

  if (req.method === "POST" && pathname === "/") {
    const form = await req.formData();
    const name = String(form.get("name") ?? "").trim().slice(0, 80);
    const message = String(form.get("message") ?? "").trim().slice(0, 500);

    if (!name || !message) {
      // Expected validation miss: 422, not a 5xx, so the run is not marked failed.
      return render(await recentEntries(), "Please fill in both fields.", 422);
    }

    // Parameterised query: values are bound, never spliced into the SQL string.
    await db.execute({
      sql: "INSERT INTO entries (name, message) VALUES (?, ?)",
      args: [name, message],
    });

    // Redirect after POST so a page refresh does not submit the form again.
    return new Response(null, { status: 303, headers: { location: "/" } });
  }

  if (req.method === "GET" && pathname === "/") {
    return render(await recentEntries());
  }

  return new Response("Not found\n", { status: 404 });
};

async function recentEntries(): Promise<Entry[]> {
  const result = await db.execute(
    "SELECT name, message, created_at FROM entries ORDER BY id DESC LIMIT 100",
  );
  return result.rows.map((row) => ({
    name: String(row.name),
    message: String(row.message),
    created_at: String(row.created_at),
  }));
}

function render(entries: Entry[], notice = "", status = 200): Response {
  const list = entries.length === 0
    ? `<p class="empty">No messages yet. Be the first to sign.</p>`
    : entries
      .map(
        (entry) => `<li>
        <p class="msg">${escapeHtml(entry.message)}</p>
        <p class="meta">${escapeHtml(entry.name)} &middot; ${escapeHtml(entry.created_at)}</p>
      </li>`,
      )
      .join("\n      ");

  const body = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Guestbook</title>
    <style>
      body { max-width: 40rem; margin: 3rem auto; padding: 0 1.25rem;
        font: 16px/1.6 system-ui, sans-serif; color: #16181d; }
      h1 { letter-spacing: -0.02em; }
      form { display: grid; gap: 0.6rem; margin: 1.5rem 0 2rem; }
      input, textarea { font: inherit; padding: 0.55rem 0.7rem;
        border: 1px solid #d7dae0; border-radius: 6px; }
      button { justify-self: start; font: inherit; padding: 0.55rem 1rem;
        border: 0; border-radius: 6px; background: #16181d; color: #fff;
        cursor: pointer; }
      .notice { color: #b4232a; }
      ul { list-style: none; padding: 0; display: grid; gap: 1rem; }
      li { border: 1px solid #e6e8ec; border-radius: 8px; padding: 0.8rem 1rem; }
      .msg { margin: 0 0 0.35rem; }
      .meta { margin: 0; color: #5c636e; font-size: 0.85rem; }
      .empty { color: #5c636e; }
    </style>
  </head>
  <body>
    <h1>Guestbook</h1>
    ${notice ? `<p class="notice">${escapeHtml(notice)}</p>` : ""}
    <form method="post" action="/">
      <input name="name" placeholder="Your name" maxlength="80" required />
      <textarea name="message" placeholder="Say hello" rows="3" maxlength="500" required></textarea>
      <button type="submit">Sign the guestbook</button>
    </form>
    <ul>
      ${list}
    </ul>
  </body>
</html>
`;

  return new Response(body, {
    status,
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

// Never write user input into HTML without escaping it first, or a message like
// "<script>...</script>" would run in every visitor's browser.
function escapeHtml(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

Go beyond what seems possible.