chore: init monorepo with GPL-3.0 license, docs, backend skeleton, frontend wiring
This commit is contained in:
commit
43cf0e277d
57 changed files with 5027 additions and 0 deletions
250
docs/database-schema.md
Normal file
250
docs/database-schema.md
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
# Схема БД Indexium
|
||||
|
||||
> PostgreSQL 16+ с расширениями `pg_trgm`, `pgvector` (опционально), `uuid-ossp`/`pgcrypto`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Расширения
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy search
|
||||
-- CREATE EXTENSION IF NOT EXISTS vector; -- pgvector, когда нужен семантический поиск
|
||||
```
|
||||
|
||||
## 2. Таблицы
|
||||
|
||||
### `authors` — авторы (зеркало GitHub users)
|
||||
|
||||
```sql
|
||||
CREATE TABLE authors (
|
||||
github_id BIGINT PRIMARY KEY, -- GitHub user ID
|
||||
login VARCHAR(39) NOT NULL UNIQUE, -- GitHub login
|
||||
avatar_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### `mods` — моды (один репозиторий = один мод на MVP)
|
||||
|
||||
```sql
|
||||
CREATE TABLE mods (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
github_repo_id BIGINT UNIQUE NOT NULL,
|
||||
slug VARCHAR(64) UNIQUE NOT NULL, -- URL-friendly, напр. "sodium-extra"
|
||||
name VARCHAR(128) NOT NULL,
|
||||
summary TEXT, -- короткое описание из манифеста
|
||||
description TEXT, -- README.md (markdown, кэшируем)
|
||||
author_github_id BIGINT NOT NULL REFERENCES authors(github_id),
|
||||
github_repo_name VARCHAR(128) NOT NULL, -- "owner/repo"
|
||||
default_branch VARCHAR(32) DEFAULT 'main',
|
||||
icon_url TEXT,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
suspicious BOOLEAN DEFAULT FALSE,
|
||||
search_vector TSVECTOR, -- материализованный FTS вектор
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mods_search ON mods USING GIN (search_vector);
|
||||
CREATE INDEX idx_mods_trgm ON mods USING GIN (name gin_trgm_ops, summary gin_trgm_ops);
|
||||
CREATE INDEX idx_mods_author ON mods (author_github_id);
|
||||
```
|
||||
|
||||
Триггер для `search_vector`:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION mods_search_vector_update() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('english', coalesce(NEW.name,'')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(NEW.summary,'')), 'B') ||
|
||||
setweight(to_tsvector('english', coalesce(NEW.description,'')), 'C');
|
||||
RETURN NEW;
|
||||
END $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_mods_search_vector
|
||||
BEFORE INSERT OR UPDATE OF name, summary, description ON mods
|
||||
FOR EACH ROW EXECUTE FUNCTION mods_search_vector_update();
|
||||
```
|
||||
|
||||
### `mod_versions` — версии / релизы
|
||||
|
||||
```sql
|
||||
CREATE TABLE mod_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
version_number VARCHAR(32) NOT NULL, -- semver из манифеста/тега
|
||||
game_versions VARCHAR(32)[] NOT NULL, -- e.g. ['1.20.1', '1.21']
|
||||
loaders VARCHAR(16)[] NOT NULL, -- e.g. ['fabric','quilt','neoforge']
|
||||
download_url TEXT NOT NULL, -- https://github.com/.../releases/download/...
|
||||
file_name VARCHAR(128) NOT NULL, -- sodium-1.2.3.jar
|
||||
file_sha256 CHAR(64) NOT NULL,
|
||||
file_size BIGINT,
|
||||
published_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(mod_id, version_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_versions_mod ON mod_versions (mod_id, published_at DESC);
|
||||
CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loaders);
|
||||
CREATE INDEX idx_versions_sha ON mod_versions (file_sha256);
|
||||
```
|
||||
|
||||
### `webhook_deliveries` — идемпотентность webhook'ов (<50ms ответ)
|
||||
|
||||
```sql
|
||||
CREATE TABLE webhook_deliveries (
|
||||
delivery_id VARCHAR(64) PRIMARY KEY, -- X-GitHub-Delivery (UUID от GitHub)
|
||||
event VARCHAR(32) NOT NULL, -- "release"
|
||||
action VARCHAR(32), -- "published"
|
||||
repo_id BIGINT,
|
||||
payload JSONB,
|
||||
processed_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
-- Уникальный индекс уже есть как PK, но явно для дедупа:
|
||||
-- INSERT INTO webhook_deliveries (...) VALUES (...) ON CONFLICT (delivery_id) DO NOTHING
|
||||
-- В handler: если affected_rows == 0 → 409 Already Processed, иначе push в Redis Streams.
|
||||
```
|
||||
|
||||
> **Почему так:** Ingestion API должен ответить `202` за <50ms. Сначала `INSERT ... ON CONFLICT DO NOTHING` в `webhook_deliveries`, только потом `XADD` в Redis. Если `delivery_id` уже есть — сразу `409` без очереди.
|
||||
|
||||
### `mod_authors` — M2M авторы/контрибьюторы
|
||||
|
||||
На MVP `mods.author_github_id` достаточно (1 репо = 1 owner). Для организаций и соавторов — нормализуем сразу, чтобы не мигрировать болезненно:
|
||||
|
||||
```sql
|
||||
CREATE TABLE mod_authors (
|
||||
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
github_id BIGINT NOT NULL REFERENCES authors(github_id) ON DELETE CASCADE,
|
||||
role VARCHAR(16) NOT NULL DEFAULT 'owner', -- 'owner' | 'contributor' | 'maintainer'
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (mod_id, github_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mod_authors_github ON mod_authors (github_id);
|
||||
-- На MVP можно оставить mods.author_github_id как денормализованный owner
|
||||
-- и дублировать его в mod_authors при создании мода (триггер или код).
|
||||
```
|
||||
|
||||
> **MVP стратегия:** оставляем `mods.author_github_id` (как сейчас в `migrations/20260906000000_init_schema.sql`) для простых запросов, но добавляем `mod_authors` когда появится первый кейс организации. В `GET /mods/:slug` отдаём `authors: [{login, role}]` вместо одиночного `author`.
|
||||
|
||||
### `mod_versions.file_size` — откуда берётся
|
||||
|
||||
В `api-spec.md` поле `file_size` возвращается клиентам. Заполняется воркером из HTTP-заголовка:
|
||||
|
||||
```sql
|
||||
-- уже в mod_versions: file_size BIGINT — bytes из Content-Length
|
||||
```
|
||||
|
||||
Алгоритм воркера (`jar_parser.rs`):
|
||||
1. `HEAD download_url` → `Content-Length` + `Accept-Ranges: bytes`.
|
||||
2. Если `Content-Length` отсутствует — fallback на `GET` с `Range: bytes=0-0` и парсинг `Content-Range`.
|
||||
3. Значение пишется в `mod_versions.file_size` при `INSERT`.
|
||||
|
||||
> GitHub CDN (`objects.githubusercontent.com`) всегда отдаёт `Content-Length` и поддерживает `Range` для release assets — проверено для `.jar` до 50MB.
|
||||
|
||||
### `dependencies` (опционально, нормализованная)
|
||||
|
||||
На MVP храним зависимости как `JSONB` в `mod_versions` или отдельной таблицей:
|
||||
|
||||
```sql
|
||||
CREATE TABLE mod_dependencies (
|
||||
version_id UUID REFERENCES mod_versions(id) ON DELETE CASCADE,
|
||||
depends_on_mod_id UUID REFERENCES mods(id), -- nullable если внешний мод не в индексе
|
||||
mod_id_str VARCHAR(64) NOT NULL, -- id из fabric.mod.json depends
|
||||
version_range VARCHAR(64), -- ">=1.0.0"
|
||||
PRIMARY KEY (version_id, mod_id_str)
|
||||
);
|
||||
```
|
||||
|
||||
## 3. Телеметрия — аналог bStats (см. docs/analytics.md)
|
||||
|
||||
```sql
|
||||
-- Полуагрегат: один пинг = одна строка, TTL 30 дней (DELETE via cron)
|
||||
CREATE TABLE mod_telemetry_pings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
server_hash CHAR(64) NOT NULL, -- sha256(server_uuid + daily_salt)
|
||||
mc_version VARCHAR(16) NOT NULL,
|
||||
loader VARCHAR(16) NOT NULL,
|
||||
os VARCHAR(16) NOT NULL,
|
||||
java_version VARCHAR(16) NOT NULL,
|
||||
player_count INT NOT NULL DEFAULT 0,
|
||||
pinged_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_telemetry_lookup ON mod_telemetry_pings (mod_id, pinged_at DESC);
|
||||
CREATE INDEX idx_telemetry_hash ON mod_telemetry_pings (server_hash, pinged_at);
|
||||
|
||||
-- Суточный агрегат — хранится навсегда
|
||||
CREATE TABLE mod_daily_stats (
|
||||
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
active_servers INT NOT NULL DEFAULT 0, -- COUNT(DISTINCT server_hash) за день
|
||||
active_players INT NOT NULL DEFAULT 0,
|
||||
breakdown_json JSONB NOT NULL, -- { mc_versions:{}, loaders:{}, os:{}, java:{}, custom:{...} }
|
||||
PRIMARY KEY (mod_id, date)
|
||||
);
|
||||
|
||||
-- Соль для анонимизации (ротация daily)
|
||||
CREATE TABLE analytics_salts (
|
||||
date DATE PRIMARY KEY,
|
||||
salt CHAR(64) NOT NULL
|
||||
);
|
||||
-- Хеш: server_hash = sha256(server_uuid || salt_for_today) — позволяет считать уникальные за день, но не трекать сквозь дни.
|
||||
-- Rate limit: Redis SET server_hash:mod_id NX EX 900 (1 пинг / 15 мин)
|
||||
-- TTL: DELETE FROM mod_telemetry_pings WHERE pinged_at < NOW() - INTERVAL '30 days' (cron hourly)
|
||||
-- Агрегация: кроном раз в час INSERT INTO mod_daily_stats ... ON CONFLICT DO UPDATE COUNT(DISTINCT server_hash)
|
||||
```
|
||||
|
||||
> Postgres хватает до ~10M пингов/мес. При росте — `SELECT create_hypertable('mod_telemetry_pings','pinged_at')` (TimescaleDB) или ClickHouse без смены схемы.
|
||||
|
||||
## 4. Пример запросов
|
||||
|
||||
### Поиск с FTS + фильтры
|
||||
|
||||
```sql
|
||||
SELECT id, name, summary, ts_rank(search_vector, query) AS rank
|
||||
FROM mods, plainto_tsquery('english', $1) query
|
||||
WHERE search_vector @@ query
|
||||
AND suspicious = false
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM mod_versions v
|
||||
WHERE v.mod_id = mods.id
|
||||
AND v.game_versions && ARRAY[$2]::varchar[]
|
||||
AND v.loaders && ARRAY[$3]::varchar[]
|
||||
)
|
||||
ORDER BY rank DESC
|
||||
LIMIT 20 OFFSET $4;
|
||||
```
|
||||
|
||||
### Fuzzy (опечатки)
|
||||
|
||||
```sql
|
||||
SELECT name, similarity(name, 'sodim') AS sml
|
||||
FROM mods
|
||||
WHERE name % 'sodim' -- оператор pg_trgm
|
||||
ORDER BY sml DESC LIMIT 10;
|
||||
```
|
||||
|
||||
## 5. Миграции
|
||||
|
||||
Хранятся в `indexium-backend/migrations/` (sqlx):
|
||||
|
||||
```
|
||||
migrations/
|
||||
20260906000000_init_schema.sql -- mods, mod_versions
|
||||
20260907000000_telemetry.sql -- mod_telemetry_pings, mod_daily_stats, analytics_salts
|
||||
```
|
||||
|
||||
Запуск: `sqlx migrate run` / `cargo sqlx migrate run`.
|
||||
|
||||
## 6. Сиды
|
||||
|
||||
Для дев-окружения: `migrations/seeds/dev.sql` — 5 фейковых модов + версии, чтобы фронт сразу имел данные.
|
||||
|
||||
## 7. Будущие расширения
|
||||
|
||||
- `pgvector` колонка `embedding vector(1536)` для семантического поиска по README.
|
||||
- Партиционирование `mod_versions` по `published_at` если >1M строк.
|
||||
- Материализованное представление `popular_mods` (top по скачиваниям).
|
||||
Loading…
Add table
Add a link
Reference in a new issue