ygoapi
Version:
TypeScript client for the YGOPRODeck API v7 - Yu-Gi-Oh! card database access
253 lines (251 loc) • 7.69 kB
JavaScript
// 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
};