Швидкий старт

6 хв читання

Швидкий старт

Отримайте робочий API поверх вашої бази даних PostgreSQL за кілька хвилин — і викликайте його зі звичайної веб-сторінки. Ваша база даних і є вашим бекендом: без фреймворку, без шаблонного коду, без бібліотеки JavaScript. Ми крок за кроком побудуємо один «Hello World»: функцію з параметром, сповіщення в реальному часі та згенерований файл.

Увесь код цього посібника доступний як готовий приклад: hello_world.sql (база даних) та index.html (веб-сторінка). Кожен endpoint докладно описано на його власній довідковій сторінці: JSON-RPC, Сповіщення в реальному часі (SSE) та Завантаження файлів (/file).

1. Встановлення

Завантажте бінарний файл

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

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

2. Налаштуйте базу даних

Створіть базу даних, службову роль, від імені якої PgArachne підключається, і завантажте схему PgArachne (sql/schema.sql знаходиться в репозиторії):

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

3. Налаштуйте та запустіть

Створіть файл pgarachne.env (сервер знаходить його в поточному каталозі). STATIC_FILES_PATH дозволяє PgArachne також віддавати веб-сторінку, яку ви створите на кроці 5, тож сторінка й API мають одне джерело (origin), і налаштування 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=…

Запустіть сервер (пароль службової ролі зчитується з PGPASSWORD або ~/.pgpass):

PGPASSWORD=pgarachne_password ./pgarachne

4. Hello World — функція з параметром

Кожен метод API — це функція PostgreSQL з єдиним аргументом jsonb. Створіть роль для веб-сторінки та функцію; JSON-об’єкт, який клієнт надсилає як params, надходить у функцію як 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;

Доступом керує сам PostgreSQL: demo_user може викликати рівно ті функції, на які йому надано право EXECUTE — нічого іншого недоступне.

5. Викличте її — з curl і з веб-сторінки

Ваша кінцева точка API вже працює. Ім’я користувача та пароль ролі PostgreSQL передаються за допомогою 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}

Той самий виклик із браузера за допомогою вбудованого fetch(). Збережіть цей код як index.html у теці з STATIC_FILES_PATH і відкрийте 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. Реальний час — нехай база даних надішле повідомлення (SSE)

PostgreSQL може публікувати повідомлення за допомогою pg_notify; PgArachne пересилає їх у браузери як Server-Sent Events. Розширте функцію: коли виклик містить "notify": true, вона додатково публікує привітання в канал 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; $$;

На сторінці спочатку підпишіться на канал, а потім викличте функцію. Вбудований EventSource не може надіслати заголовок Authorization, тому потік читається через fetch() (приблизно 15 рядків):

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
Варто знати: NOTIFY є транзакційним — PostgreSQL доставляє його лише після фіксації транзакції функції, тобто саме тоді, коли повертається відповідь на виклик. Підписуйтеся до виклику: PostgreSQL не зберігає сповіщення для слухачів, які ще не підключені. Канали SSE не обмежені роллю (дивіться Сповіщення в реальному часі (SSE)).

7. Завантажте файл

Функцію, що повертає рядки (path, content, mime_type, store_only), можна завантажити через кінцеву точку /file — як сирі байти, без base64 усередині JSON. Ця повертає 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;

На сторінці можна показати вміст або запропонувати його для завантаження (поверніть більше рядків — і автоматично отримаєте 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);

Спробуйте з 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. Більше в розділі Завантаження файлів (/file).

Перш ніж іти в продакшен

  • Використовуйте HTTPS. Облікові дані Basic передаються з кожним запитом; див. Розгортання та HTTPS. Змініть демонстраційні паролі.
  • Зберігайте облікові дані лише в пам’яті (змінна JavaScript), ніколи в localStorage. Щоб не надсилати пароль повторно, задайте JWT_SECRET і отримуйте токен із POST /db/my_database/token (див. Безпека та автентифікація).
  • Веб-сторінка на іншому origin (CDN, інший порт)? Задайте ALLOWED_ORIGINS=https://app.example.com; міждоменні запити за замовчуванням блокуються.
  • Найменші привілеї: одна роль PostgreSQL на кожен тип користувачів, EXECUTE лише на потрібні їм функції, Row-Level Security для даних окремих користувачів та ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, щоб нові функції не відкривалися випадково.

8. Підключіть ШІ-агента (MCP)

Надайте ті самі функції як інструменти для Claude Desktop або Cursor через кінцеву точку MCP:

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

Повні інструкції з підключення дивіться в розділі Model Context Protocol (MCP).

9. Отримайте специфікацію OpenAPI (Swagger, Postman, генерація коду)

Кожна відкрита функція також описана в документі OpenAPI 3.1, відфільтрованому до того, що може викликати автентифікований користувач:

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

Імпортуйте URL безпосередньо в Swagger UI, Postman або генератор коду OpenAPI. Кожен метод, як і раніше, виконується через єдину кінцеву точку JSON-RPC з кроку 5 — чому специфікація перелічує окремий шлях для кожного методу без відповідного реального маршруту, пояснено в розділі Архітектурні рішення.

Це все. Прочитайте JSON-RPC, щоб ознайомитися з форматом запитів, написанням функцій, помилками та ідемпотентністю, або порівняння з PostgREST, щоб зрозуміти, яке місце посідає PgArachne.