Szybki start

6 min czytania

Szybki start

Uzyskaj działające API na bazie swojej bazy danych PostgreSQL w kilka minut — i wywołaj je ze zwykłej strony internetowej. Twoja baza danych jest Twoim backendem: bez frameworka, bez szablonowego kodu, bez biblioteki JavaScript. Zbudujemy krok po kroku jeden „Hello World": funkcję z parametrem, powiadomienie w czasie rzeczywistym i wygenerowany plik.

Cały kod tego przewodnika jest dostępny jako gotowy przykład: hello_world.sql (baza danych) i index.html (strona internetowa). Każdy endpoint jest szczegółowo opisany na własnej stronie referencyjnej: JSON-RPC, Powiadomienia w czasie rzeczywistym (SSE) i Pobieranie plików (/file).

1. Instalacja

Pobierz plik binarny

# macOS (Homebrew)
brew install heptau/tap/pgarachne

# or download the latest release for your OS:
#   https://github.com/heptau/pgarachne/releases/latest

2. Przygotuj bazę danych

Utwórz bazę danych, rolę serwisową, jako którą łączy się PgArachne, i wczytaj schemat PgArachne (sql/schema.sql znajduje się w repozytorium):

createdb my_database
psql -d my_database -c "CREATE ROLE pgarachne LOGIN PASSWORD 'pgarachne_password'"
psql -d my_database -f sql/schema.sql

3. Skonfiguruj i uruchom

Utwórz plik pgarachne.env (serwer znajdzie go w bieżącym katalogu). Dzięki STATIC_FILES_PATH PgArachne udostępni także stronę internetową, którą utworzysz w kroku 5, więc strona i API mają jedno źródło (origin) i nie jest potrzebna konfiguracja CORS:

DB_HOST=localhost
DB_PORT=5432
DB_USER=pgarachne
DB_SSLMODE=disable                         # local PostgreSQL without TLS only
STATIC_FILES_PATH=/path/to/web             # folder with your index.html
# Optional: enables JWT sessions (POST /db//token); generate with openssl rand -hex 32
# JWT_SECRET=…

Uruchom serwer (hasło roli serwisowej jest odczytywane z PGPASSWORD lub ~/.pgpass):

PGPASSWORD=pgarachne_password ./pgarachne

4. Hello World — funkcja z parametrem

Każda metoda API to funkcja PostgreSQL z jednym argumentem jsonb. Utwórz rolę dla strony internetowej i funkcję; obiekt JSON, który klient wysyła jako params, trafia do funkcji jako payload:

CREATE ROLE demo_user WITH LOGIN PASSWORD 'user_password';
GRANT USAGE ON SCHEMA api TO demo_user;

CREATE OR REPLACE FUNCTION api.hello_world(payload jsonb DEFAULT '{}'::jsonb)
RETURNS json LANGUAGE sql AS $$
    SELECT json_build_object(
        'message', 'Hello, ' || COALESCE(payload->>'name', 'World') || '!');
$$;
GRANT EXECUTE ON FUNCTION api.hello_world(jsonb) TO demo_user;

Dostępem steruje sam PostgreSQL: demo_user może wywoływać dokładnie te funkcje, do których otrzymał uprawnienie EXECUTE — nic więcej nie jest osiągalne.

5. Wywołaj ją — z curl i ze strony internetowej

Twój punkt końcowy API już działa. Nazwa użytkownika i hasło roli PostgreSQL są wysyłane za pomocą uwierzytelniania HTTP Basic:

curl -X POST http://localhost:8080/db/my_database/jsonrpc \
  -u demo_user:user_password \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"api.hello_world","params":{"name":"Alice"},"id":1}'
# → {"jsonrpc":"2.0","result":{"message":"Hello, Alice!"},"id":1}

To samo wywołanie z przeglądarki, za pomocą wbudowanego fetch(). Zapisz poniższy kod jako index.html w folderze z STATIC_FILES_PATH i otwórz http://localhost:8080/:

<input id="name" value="Alice">
<button id="hello">Say hello</button>
<pre id="out"></pre>

<script>
// HTTP Basic header: "user:password" in base64 (TextEncoder handles non-ASCII)
const bytes = new TextEncoder().encode("demo_user:user_password");
const auth = "Basic " + btoa(String.fromCharCode(...bytes));
const base = location.origin + "/db/my_database";      // /{API_PREFIX}/{database}

async function rpc(method, params) {
  const res = await fetch(base + "/jsonrpc", {
    method: "POST",
    headers: { "Authorization": auth, "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
  });
  const body = await res.json();
  if (!res.ok || body.error) throw new Error(body.error.message);
  return body.result;
}

document.getElementById("hello").onclick = async () => {
  const result = await rpc("api.hello_world", { name: document.getElementById("name").value });
  document.getElementById("out").textContent = result.message;      // Hello, Alice!
};
</script>

6. Czas rzeczywisty — niech baza danych wyśle wiadomość (SSE)

PostgreSQL może publikować wiadomości za pomocą pg_notify; PgArachne przekazuje je do przeglądarek jako Server-Sent Events. Rozszerz funkcję: gdy wywołujący przekaże "notify": true, funkcja dodatkowo opublikuje powitanie na kanale hello:

CREATE OR REPLACE FUNCTION api.hello_world(payload jsonb DEFAULT '{}'::jsonb)
RETURNS json LANGUAGE plpgsql AS $$
DECLARE
    greeting text := 'Hello, ' || COALESCE(payload->>'name', 'World') || '!';
BEGIN
    IF COALESCE((payload->>'notify')::boolean, false) THEN
        PERFORM pg_notify('hello', json_build_object('message', greeting)::text);
    END IF;
    RETURN json_build_object('message', greeting);
END; $$;

Na stronie najpierw zasubskrybuj kanał, a potem wywołaj funkcję. Wbudowany EventSource nie potrafi wysłać nagłówka Authorization, dlatego strumień jest odczytywany przez fetch() (około 15 linii):

async function listen(channels, onMessage) {
  const res = await fetch(base + "/sse?channels=" + encodeURIComponent(channels), {
    headers: { "Authorization": auth },
  });
  if (!res.ok) throw new Error((await res.json()).error);
  const reader = res.body.getReader(), decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let end;
    while ((end = buffer.indexOf("\n\n")) >= 0) {        // an event ends with a blank line
      const event = buffer.slice(0, end);
      buffer = buffer.slice(end + 2);
      for (const line of event.split("\n"))
        if (line.startsWith("data: ")) onMessage(JSON.parse(line.slice(6)));
    }
  }
}

listen("hello", (m) => console.log("event:", m.data.message));   // m = {channel, data}
rpc("api.hello_world", { name: "Alice", notify: true });             // the event arrives immediately
Warto wiedzieć: NOTIFY jest transakcyjne — PostgreSQL dostarcza je dopiero po zatwierdzeniu transakcji funkcji, czyli dokładnie w chwili zwrócenia odpowiedzi. Subskrybuj przed wywołaniem: PostgreSQL nie przechowuje powiadomień dla nasłuchujących, którzy nie są jeszcze połączeni. Kanały SSE nie są ograniczone do roli (zobacz Powiadomienia w czasie rzeczywistym (SSE)).

7. Pobierz plik

Funkcję zwracającą wiersze (path, content, mime_type, store_only) można pobrać przez punkt końcowy /file — jako surowe bajty, bez base64 wewnątrz JSON. Ta zwraca plik hello-world.md:

CREATE OR REPLACE FUNCTION api.hello_world_file(payload jsonb DEFAULT '{}'::jsonb)
RETURNS TABLE (path text, content bytea, mime_type text, store_only boolean)
LANGUAGE sql AS $$
    SELECT 'hello-world.md',
           convert_to('# Hello, ' || COALESCE(payload->>'name', 'World') || E'!\n\nGenerated by PostgreSQL at ' || now() || E'.\n', 'UTF8'),
           'text/markdown',
           false;
$$;
GRANT EXECUTE ON FUNCTION api.hello_world_file(jsonb) TO demo_user;

Na stronie możesz wyświetlić zawartość lub zaoferować ją do pobrania (zwróć więcej wierszy, a automatycznie otrzymasz ZIP):

async function getFile() {
  const res = await fetch(base + "/file", {
    method: "POST",
    headers: { "Authorization": auth, "Content-Type": "application/json" },
    body: JSON.stringify({ method: "api.hello_world_file", params: { name: "Alice" } }),
  });
  if (!res.ok) throw new Error((await res.json()).error.message);   // errors are JSON, never binary
  return res.blob();
}

// show it:
console.log(await (await getFile()).text());

// or download it:
const url = URL.createObjectURL(await getFile());
const a = Object.assign(document.createElement("a"), { href: url, download: "hello-world.md" });
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);

Wypróbuj z curl: curl -u demo_user:user_password -H "Content-Type: application/json" -d '{"method":"api.hello_world_file"}' http://localhost:8080/db/my_database/file -OJ. Więcej w Pobieranie plików (/file).

Zanim przejdziesz na produkcję

  • Używaj HTTPS. Dane uwierzytelniające Basic są wysyłane z każdym żądaniem; zobacz Wdrożenie i HTTPS. Zmień hasła z przykładu.
  • Trzymaj dane uwierzytelniające tylko w pamięci (zmienna JavaScript), nigdy w localStorage. Aby nie wysyłać hasła za każdym razem, ustaw JWT_SECRET i pobierz token z POST /db/my_database/token (zobacz Bezpieczeństwo i uwierzytelnianie).
  • Strona internetowa na innym origin (CDN, inny port)? Ustaw ALLOWED_ORIGINS=https://app.example.com; żądania cross-origin są domyślnie blokowane.
  • Najmniejsze uprawnienia: jedna rola PostgreSQL na rodzaj użytkownika, EXECUTE tylko na funkcjach, których potrzebują, Row-Level Security dla danych poszczególnych użytkowników oraz ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, aby nowe funkcje nie były udostępniane przypadkowo.

8. Podłącz agenta AI (MCP)

Udostępnij te same funkcje jako narzędzia dla Claude Desktop lub Cursor przez punkt końcowy MCP:

POST http://localhost:8080/db/my_database/mcp

Pełne instrukcje połączenia znajdziesz w Model Context Protocol (MCP).

9. Pobierz specyfikację OpenAPI (Swagger, Postman, generowanie kodu)

Każda udostępniona funkcja jest też opisana w dokumencie OpenAPI 3.1, przefiltrowanym do tego, co może wywołać uwierzytelniony użytkownik:

curl http://localhost:8080/db/my_database/openapi.json \
  -u demo_user:user_password
# or as YAML: .../openapi.yaml

Zaimportuj adres URL bezpośrednio do Swagger UI, Postmana lub generatora kodu OpenAPI. Każda metoda nadal przechodzi przez jeden punkt końcowy JSON-RPC z kroku 5 — w Decyzjach architektonicznych wyjaśniamy, dlaczego specyfikacja wymienia ścieżkę dla każdej metody bez odpowiadającej jej rzeczywistej trasy.

To wszystko. Przeczytaj JSON-RPC, aby poznać format żądań, pisanie funkcji, błędy i idempotencję, lub porównanie z PostgREST, aby zrozumieć, gdzie plasuje się PgArachne.