UNPKG

ygoapi

Version:

TypeScript client for the YGOPRODeck API v7 - Yu-Gi-Oh! card database access

253 lines (251 loc) 7.69 kB
// src/index.ts class YgoApiError extends Error { statusCode; constructor(statusCode, message) { super(message); this.statusCode = statusCode; this.name = "YgoApiError"; } } class YgoApi { baseURL = "https://db.ygoprodeck.com/api/v7"; headers; cache; cacheTtl; retryConfig; fallbackConfig; constructor(options) { this.headers = { "Content-Type": "application/json", ...options?.headers }; this.cache = options?.cache; this.cacheTtl = options?.cacheTtl ?? 300000; this.retryConfig = { maxAttempts: options?.retry?.maxAttempts ?? 3, baseDelay: options?.retry?.baseDelay ?? 1000, maxDelay: options?.retry?.maxDelay ?? 1e4, backoffFactor: options?.retry?.backoffFactor ?? 2 }; this.fallbackConfig = { urls: options?.fallback?.urls ?? [], timeout: options?.fallback?.timeout ?? 5000 }; } buildQueryString(params) { if (!params) return ""; const query = new URLSearchParams; Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null) return; if (Array.isArray(value)) { query.append(key, value.join(",")); } else if (typeof value === "boolean") { query.append(key, value.toString()); } else { query.append(key, String(value)); } }); const queryString = query.toString(); return queryString ? `?${queryString}` : ""; } sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } getCacheKey(endpoint, params) { const sortedParams = params ? JSON.stringify(params, Object.keys(params).sort()) : ""; return `ygoapi:${endpoint}:${sortedParams}`; } async getCached(cacheKey) { if (!this.cache) return null; try { const cached = await this.cache.get(cacheKey); return cached ? JSON.parse(cached) : null; } catch { return null; } } async setCached(cacheKey, data) { if (!this.cache) return; try { await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl); } catch {} } async request(endpoint, params) { if (params && (params.offset !== undefined && params.num === undefined || params.num !== undefined && params.offset === undefined)) { throw new YgoApiError(400, "You cannot use only one of 'offset' or 'num'. You must use both or none."); } const cacheKey = this.getCacheKey(endpoint, params); const cached = await this.getCached(cacheKey); if (cached) return cached; const urls = [this.baseURL, ...this.fallbackConfig.urls]; let lastError; for (const baseUrl of urls) { const url = `${baseUrl}${endpoint}${this.buildQueryString(params)}`; for (let attempt = 1;attempt <= this.retryConfig.maxAttempts; attempt++) { try { const controller = new AbortController; const timeoutId = setTimeout(() => controller.abort(), this.fallbackConfig.timeout); const response = await fetch(url, { method: "GET", headers: this.headers, signal: controller.signal }); clearTimeout(timeoutId); const data = await response.json(); if (!response.ok) { throw new YgoApiError(response.status, data.error || `API request failed with status ${response.status}`); } await this.setCached(cacheKey, data); return data; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (error instanceof YgoApiError && error.statusCode < 500) { break; } if (attempt < this.retryConfig.maxAttempts) { const delay = Math.min(this.retryConfig.baseDelay * this.retryConfig.backoffFactor ** (attempt - 1), this.retryConfig.maxDelay); await this.sleep(delay); } } } } if (lastError instanceof YgoApiError) { throw lastError; } throw new YgoApiError(500, `Network error: ${lastError?.message || "Unknown error"}`); } async getCardInfo(params) { return this.request("/cardinfo.php", params); } async getCardByName(name) { try { const response = await this.getCardInfo({ name }); return response.data[0] || null; } catch (error) { if (error instanceof YgoApiError && error.statusCode === 400) { return null; } throw error; } } async getCardById(id) { try { const response = await this.getCardInfo({ id }); return response.data[0] || null; } catch (error) { if (error instanceof YgoApiError && error.statusCode === 400) { return null; } throw error; } } async searchCards(fname, params) { return this.getCardInfo({ ...params, fname }); } async getCardsByArchetype(archetype, params) { return this.getCardInfo({ ...params, archetype }); } async getCardsBySet(cardset, params) { return this.getCardInfo({ ...params, cardset }); } async getRandomCard() { const response = await this.request("/randomcard.php"); return response.data[0]; } async getAllCardSets() { return this.request("/cardsets.php"); } async getCardSetInfo(setcode) { return this.request("/cardsetsinfo.php", { setcode }); } async getAllArchetypes() { return this.request("/archetypes.php"); } async checkDatabaseVersion() { const response = await this.request("/checkDBVer.php"); return response[0]; } async getStapleCards(params) { return this.getCardInfo({ ...params, staple: "yes" }); } async getCardsByFormat(format, params) { return this.getCardInfo({ ...params, format }); } async getCardsByGenesysFormat(params) { return this.getCardInfo({ ...params, format: "genesys", misc: "yes" }); } async getBanlistCards(banlist, params) { return this.getCardInfo({ ...params, banlist }); } async getCardsWithPagination(num, offset, params) { return this.getCardInfo({ ...params, num, offset }); } async getCardsByType(type, params) { return this.getCardInfo({ ...params, type }); } async getCardsByAttribute(attribute, params) { return this.getCardInfo({ ...params, attribute }); } async getCardsByRace(race, params) { return this.getCardInfo({ ...params, race }); } async getCardsByLevel(level, params) { return this.getCardInfo({ ...params, level }); } async getCardsByATK(atk, params) { return this.getCardInfo({ ...params, atk }); } async getCardsByDEF(def, params) { return this.getCardInfo({ ...params, def }); } async getCardsWithMiscInfo(params) { return this.getCardInfo({ ...params, misc: "yes" }); } } function buildComparison(operator, value) { return `${operator}${value}`; } function getCardImages(card) { const [defaultImage, ...alternates] = card.card_images; return { default: defaultImage, alternates }; } function isMonsterCard(card) { return !card.type.includes("Spell") && !card.type.includes("Trap"); } function isSpellCard(card) { return card.type.includes("Spell"); } function isTrapCard(card) { return card.type.includes("Trap"); } function isExtraDeckMonster(card) { const extraDeckTypes = [ "Fusion Monster", "Link Monster", "Pendulum Effect Fusion Monster", "Synchro Monster", "Synchro Pendulum Effect Monster", "Synchro Tuner Monster", "XYZ Monster", "XYZ Pendulum Effect Monster" ]; return extraDeckTypes.includes(card.type); } export { isTrapCard, isSpellCard, isMonsterCard, isExtraDeckMonster, getCardImages, buildComparison, YgoApiError, YgoApi };