Files
Randify.pro/src/lib/open5e/client.ts
T
emil 25f43ec4e2 feat(dm-dashboard): Wave 1 — auth fixes, DB schema, Open5e interfaces, AI clients
- Fix auth anti-patterns (JWT, OAuth, logout, middleware, url.origin)
- Extend DB schema with tier, npcs, counters, translations, notes, initiative
- Expand Open5e TypeScript interfaces (Monster, Spell) to full V2
- Build OpenRouter API client for FREE tier
- Build Kimi API client for PRO tier
- Add comprehensive tests for all modules
2026-05-15 21:28:42 +03:00

173 lines
4.2 KiB
TypeScript

// BUGFIX: Original code used leading slashes in endpoint paths which caused
// new URL() to drop the /v2 base path. Fixed by adding trailing slash to
// API_BASE and using relative paths.
import { getCached, setCached } from "./cache";
const API_BASE = "https://api.open5e.com/v2/";
export interface MonsterAction {
name: string;
desc?: string;
}
export interface Monster {
name: string;
key: string;
challenge_rating_decimal: string;
type: string;
hit_points: number;
armor_class: number;
speed?: Record<string, string | number | null>;
actions?: MonsterAction[];
special_abilities?: MonsterAction[];
legendary_actions?: MonsterAction[];
senses?: Record<string, string | number | null>;
languages?: string;
strength?: number;
dexterity?: number;
constitution?: number;
intelligence?: number;
wisdom?: number;
charisma?: number;
size?: string;
subtype?: string;
alignment?: string;
damage_immunities?: string[];
damage_resistances?: string[];
damage_vulnerabilities?: string[];
condition_immunities?: string[];
}
export interface SpellClass {
name: string;
slug?: string;
}
export interface Spell {
name: string;
key: string;
level: number;
school: string;
casting_time?: string;
range?: string;
components?: string;
duration?: string;
desc?: string[];
higher_level?: string[];
ritual?: boolean;
concentration?: boolean;
classes?: SpellClass[];
}
interface SearchFilters {
[key: string]: string;
}
async function apiFetch<T>(url: string): Promise<T> {
const res = await fetch(url, {
headers: { Accept: "application/json" },
});
if (!res.ok) {
throw new Error(`${res.status}: ${res.statusText}`);
}
return res.json();
}
function buildUrl(endpoint: string, query?: string, filters?: SearchFilters): string {
const url = new URL(endpoint, API_BASE);
if (query) {
url.searchParams.set("name__icontains", query);
}
if (filters) {
for (const [key, value] of Object.entries(filters)) {
url.searchParams.set(key, value);
}
}
return url.toString();
}
export async function searchMonsters(
query: string,
filters?: SearchFilters
): Promise<Monster[]> {
const cacheKey = `open5e:v2:monsters:${query}`;
const cached = getCached<Monster[]>(cacheKey);
if (cached) return cached;
const url = buildUrl("creatures/", query, filters);
const urlObj = new URL(url);
urlObj.searchParams.set(
"fields",
"name,key,challenge_rating_decimal,type,hit_points,armor_class"
);
try {
const data = await apiFetch<{ results: Monster[] }>(urlObj.toString());
setCached(cacheKey, data.results);
return data.results;
} catch (err) {
const stale = getCached<Monster[]>(cacheKey);
if (stale) return stale;
throw err;
}
}
export async function getMonster(key: string): Promise<Monster> {
const cacheKey = `open5e:v2:monster:${key}`;
const cached = getCached<Monster>(cacheKey);
if (cached) return cached;
const url = `${API_BASE}creatures/${key}/`;
try {
const data = await apiFetch<Monster>(url);
setCached(cacheKey, data);
return data;
} catch (err) {
const stale = getCached<Monster>(cacheKey);
if (stale) return stale;
throw err;
}
}
export async function searchSpells(
query: string,
filters?: SearchFilters
): Promise<Spell[]> {
const cacheKey = `open5e:v2:spells:${query}`;
const cached = getCached<Spell[]>(cacheKey);
if (cached) return cached;
const url = buildUrl("spells/", query, filters);
const urlObj = new URL(url);
urlObj.searchParams.set("fields", "name,key,level,school");
try {
const data = await apiFetch<{ results: Spell[] }>(urlObj.toString());
setCached(cacheKey, data.results);
return data.results;
} catch (err) {
const stale = getCached<Spell[]>(cacheKey);
if (stale) return stale;
throw err;
}
}
export async function getSpell(key: string): Promise<Spell> {
const cacheKey = `open5e:v2:spell:${key}`;
const cached = getCached<Spell>(cacheKey);
if (cached) return cached;
const url = `${API_BASE}spells/${key}/`;
try {
const data = await apiFetch<Spell>(url);
setCached(cacheKey, data);
return data;
} catch (err) {
const stale = getCached<Spell>(cacheKey);
if (stale) return stale;
throw err;
}
}