Add full sortof codebase: API, drain workers, frontend, schema, specs

This commit is contained in:
2026-05-04 03:27:54 +00:00
parent acda2c90f8
commit 55d3794bfb
43 changed files with 13375 additions and 53 deletions

127
init/01_schema.sql Normal file
View File

@@ -0,0 +1,127 @@
-- pzsort schema
-- Run as: psql -U pzsort -d pzsort -f schema.sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- -----------------------------------------------------------------------------
-- workshop_meta
-- Cheap, refreshed often via Steam ISteamRemoteStorage/GetPublishedFileDetails.
-- Keyed by Steam publishedfileid (text to avoid bigint surprises).
-- time_updated is the cache-invalidation key for mod_parsed.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS workshop_meta (
workshop_id TEXT PRIMARY KEY,
title TEXT,
description TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
creator_steamid TEXT,
time_created BIGINT, -- unix ts from Steam
time_updated BIGINT NOT NULL, -- unix ts; cache invalidation key
file_size BIGINT,
preview_url TEXT,
consumer_app_id INTEGER, -- 108600 for PZ
visibility INTEGER, -- 0=public, 1=friends, 2=private
banned BOOLEAN NOT NULL DEFAULT FALSE,
last_checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS workshop_meta_last_checked_idx
ON workshop_meta (last_checked_at);
-- -----------------------------------------------------------------------------
-- mod_parsed
-- Expensive: requires DepotDownloader fetch. Only refreshed when
-- workshop_meta.time_updated changes vs parsed_at_time_updated.
-- One workshop item can yield N rows (multi-mod packages).
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS mod_parsed (
workshop_id TEXT NOT NULL REFERENCES workshop_meta(workshop_id) ON DELETE CASCADE,
mod_id TEXT NOT NULL, -- mod.info `id=`
name TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'undefined',
requirements TEXT[] NOT NULL DEFAULT '{}',
load_after TEXT[] NOT NULL DEFAULT '{}',
load_before TEXT[] NOT NULL DEFAULT '{}',
incompatible_mods TEXT[] NOT NULL DEFAULT '{}',
load_first TEXT NOT NULL DEFAULT 'off',
load_last TEXT NOT NULL DEFAULT 'off',
tags TEXT[] NOT NULL DEFAULT '{}',
maps TEXT[] NOT NULL DEFAULT '{}', -- map folder names
raw_mod_info TEXT, -- original file for debugging
version_min TEXT, -- e.g. 41.55
parsed_at_time_updated BIGINT NOT NULL, -- snapshot of workshop_meta.time_updated at parse
parsed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (workshop_id, mod_id)
);
CREATE INDEX IF NOT EXISTS mod_parsed_mod_id_idx ON mod_parsed (mod_id);
-- -----------------------------------------------------------------------------
-- download_jobs
-- Work queue for the DepotDownloader worker. One job per workshop_id.
-- Worker dequeues (status='queued') ORDER BY priority DESC, created_at ASC.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS download_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workshop_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued', -- queued|downloading|parsing|done|failed
priority INTEGER NOT NULL DEFAULT 0, -- higher first
attempts INTEGER NOT NULL DEFAULT 0,
error TEXT,
requested_by TEXT, -- IP hash or user token; for rate limiting
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS download_jobs_dequeue_idx
ON download_jobs (status, priority DESC, created_at ASC)
WHERE status = 'queued';
CREATE INDEX IF NOT EXISTS download_jobs_workshop_idx
ON download_jobs (workshop_id);
-- Trigger: keep updated_at fresh
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS download_jobs_touch ON download_jobs;
CREATE TRIGGER download_jobs_touch
BEFORE UPDATE ON download_jobs
FOR EACH ROW
EXECUTE FUNCTION touch_updated_at();
-- -----------------------------------------------------------------------------
-- collections
-- Cache for ISteamRemoteStorage/GetCollectionDetails results.
-- Collections expand to N child workshop_ids; we cache that mapping.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS collections (
collection_id TEXT PRIMARY KEY,
title TEXT,
child_workshop_ids TEXT[] NOT NULL DEFAULT '{}',
last_fetched_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- -----------------------------------------------------------------------------
-- sort_requests
-- Optional: log of submitted sort jobs for debugging + abuse triage.
-- Not required for sort to function. Keep TTL short via a cron.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sort_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
input_workshop_ids TEXT[] NOT NULL,
input_collection_id TEXT,
cache_hits INTEGER NOT NULL DEFAULT 0,
cache_misses INTEGER NOT NULL DEFAULT 0,
requested_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS sort_requests_created_idx
ON sort_requests (created_at);