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

Zkracovač URL

Pošlete dlouhou URL, dostanete krátký kód a přesměrování přes 302.

libsqlredirectsstorage

Pošlete URL na /api/links a dostanete krátký kód; návštěva /<code> odpoví přesměrováním 302 na původní odkaz. Odkazy jsou v libsql databázi, která vznikne a propojí se při nasazení jako LINKS_DB, takže connection string nenese žádný token a v kódu není žádné heslo.

Kódy jsou krátké a náhodné. Když je nějaký už obsazený, vložení selže na primárním klíči a zkracovač prostě zkusí jiný, takže se kolize řeší bez služby na unikátní identifikátory. Přijímají se jen http a https URL; cokoli jiného skončí chybou 422, aby zkracovač nikdy nepřesměroval návštěvníka na javascript: nebo file: URL.

kód
main.ts
// A URL shortener. POST a long URL to /api/links and get back a short code;
// visiting /<code> redirects there with a 302. Links live in a libsql database
// that was created and linked for you at deploy time: its connection string
// arrives in LINKS_DB as a tokenless URL, with credentials injected by the
// platform at the network edge, so no secret ever appears in this code.

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

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

type Link = { code: string; url: string; created_at: string };

const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";

export default async (req: Request): Promise<Response> => {
  await db.execute(
    `CREATE TABLE IF NOT EXISTS links (
       code TEXT PRIMARY KEY,
       url TEXT NOT NULL,
       created_at TEXT NOT NULL DEFAULT (datetime('now'))
     )`,
  );

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

  // Create a short link. Body: { "url": "https://..." }
  if (req.method === "POST" && pathname === "/api/links") {
    const body = await req.json().catch(() => null);
    const target = typeof body?.url === "string" ? body.url.trim() : "";

    // Only http(s) links are allowed. Reject anything else with a 422 so we
    // never store a "javascript:" or "file:" URL and redirect a visitor to it.
    if (!isHttpUrl(target)) {
      return json({ error: "Provide an http or https URL." }, 422);
    }

    const code = await createUniqueLink(target);
    return json({ code, url: target, short: `${url.origin}/${code}` }, 201);
  }

  // Follow a short link: GET /<code> -> 302 redirect to the stored URL.
  const code = pathname.slice(1);
  if (req.method === "GET" && /^[a-z0-9]+$/.test(code)) {
    const found = await db.execute({
      sql: "SELECT url FROM links WHERE code = ?",
      args: [code],
    });
    if (found.rows.length === 0) {
      return json({ error: "Unknown code." }, 404);
    }
    return new Response(null, {
      status: 302,
      headers: { location: String(found.rows[0].url) },
    });
  }

  // Index: a form plus the most recent links.
  if (req.method === "GET" && pathname === "/") {
    return render(await recentLinks(), url.origin);
  }

  return json({ error: "Not found." }, 404);
};

// Insert with a fresh random code, retrying if that code is already taken.
// The PRIMARY KEY makes a duplicate insert fail, which we catch and retry.
async function createUniqueLink(target: string): Promise<string> {
  for (let attempt = 0; attempt < 5; attempt++) {
    const code = randomCode();
    try {
      await db.execute({
        sql: "INSERT INTO links (code, url) VALUES (?, ?)",
        args: [code, target],
      });
      return code;
    } catch (error) {
      if (String(error).includes("UNIQUE")) continue; // collision: try again
      throw error;
    }
  }
  throw new Error("could not allocate a unique code");
}

function randomCode(length = 6): string {
  const bytes = crypto.getRandomValues(new Uint8Array(length));
  let code = "";
  for (const byte of bytes) code += ALPHABET[byte % ALPHABET.length];
  return code;
}

function isHttpUrl(value: string): boolean {
  try {
    const parsed = new URL(value);
    return parsed.protocol === "http:" || parsed.protocol === "https:";
  } catch {
    return false;
  }
}

async function recentLinks(): Promise<Link[]> {
  const result = await db.execute(
    "SELECT code, url, created_at FROM links ORDER BY created_at DESC LIMIT 50",
  );
  return result.rows.map((row) => ({
    code: String(row.code),
    url: String(row.url),
    created_at: String(row.created_at),
  }));
}

function render(links: Link[], origin: string): Response {
  const rows = links.length === 0
    ? `<p class="empty">No links yet.</p>`
    : links
      .map(
        (link) => `<li>
        <a href="/${link.code}">/${link.code}</a>
        <span>${escapeHtml(link.url)}</span>
      </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>URL shortener</title>
    <style>
      body { max-width: 42rem; margin: 3rem auto; padding: 0 1.25rem;
        font: 16px/1.6 system-ui, sans-serif; color: #16181d; }
      h1 { letter-spacing: -0.02em; }
      code { font-family: ui-monospace, monospace; }
      ul { list-style: none; padding: 0; display: grid; gap: 0.6rem; }
      li { display: grid; gap: 0.15rem; border: 1px solid #e6e8ec;
        border-radius: 8px; padding: 0.6rem 0.9rem; }
      li a { font-family: ui-monospace, monospace; color: #2f6feb;
        text-decoration: none; }
      li span { color: #5c636e; font-size: 0.85rem; word-break: break-all; }
      .empty { color: #5c636e; }
    </style>
  </head>
  <body>
    <h1>URL shortener</h1>
    <p>Create a link with a POST request:</p>
    <pre><code>curl -X POST ${origin}/api/links \\
  -H "content-type: application/json" \\
  -d '{"url":"https://frontback.eu"}'</code></pre>
    <h2>Recent links</h2>
    <ul>
      ${rows}
    </ul>
  </body>
</html>
`;

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

function json(data: unknown, status = 200): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: { "content-type": "application/json; charset=utf-8" },
  });
}

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.