import type CacheInterface from './Psr/SimpleCache/CacheInterface' import LocalStorageCache from './Psr/SimpleCache/LocalStorageCache' import MemoryCache from './Psr/SimpleCache/MemoryCache' import Query from './Query' import Endpoint from './endpoints/Endpoint' import SimpleEndpoint from './endpoints/SimpleEndpoint' import type { Card, CardResume, Endpoints, Serie, SerieList, SetList, StringEndpoint, SupportedLanguages, Set as TCGdexSet } from './interfaces' import CardModel from './models/Card' import CardResumeModel from './models/CardResume' import SerieModel from './models/Serie' import SerieResume from './models/SerieResume' import SetModel from './models/Set' import SetResumeModel from './models/SetResume' import StringEndpointModel from './models/StringEndpoint' import { ENDPOINTS, detectContext } from './utils' import { version } from './version' export default class TCGdex { /** * How the remote data is going to be fetched */ public static fetch: typeof fetch = fetch /** * @deprecated to change the lang use {@link TCGdex.getLang} and {@link TCGdex.setLang} */ public static defaultLang: SupportedLanguages = 'en' /** * the previously hidden caching system used by TCGdex to not kill the API */ public cache: CacheInterface = detectContext() === 'browser' ? new LocalStorageCache('tcgdex-cache') : new MemoryCache() /** * the default cache TTL, only subsequent requests will have their ttl changed */ public cacheTTL = 60 * 60 public readonly card = new Endpoint(this, CardModel, CardResumeModel, 'cards') public readonly set = new Endpoint(this, SetModel, SetResumeModel, 'sets') public readonly serie = new Endpoint(this, SerieModel, SerieResume, 'series') public readonly type = new SimpleEndpoint(this, StringEndpointModel, 'types') public readonly retreat = new SimpleEndpoint(this, StringEndpointModel, 'retreats') public readonly rarity = new SimpleEndpoint(this, StringEndpointModel, 'rarities') public readonly illustrator = new SimpleEndpoint(this, StringEndpointModel, 'illustrators') public readonly hp = new SimpleEndpoint(this, StringEndpointModel, 'hp') public readonly categorie = new SimpleEndpoint(this, StringEndpointModel, 'categories') public readonly dexID = new SimpleEndpoint(this, StringEndpointModel, 'dex-ids') public readonly energyType = new SimpleEndpoint(this, StringEndpointModel, 'energy-types') public readonly regulationMark = new SimpleEndpoint(this, StringEndpointModel, 'regulation-marks') public readonly stage = new SimpleEndpoint(this, StringEndpointModel, 'stages') public readonly suffixe = new SimpleEndpoint(this, StringEndpointModel, 'suffixes') public readonly trainerType = new SimpleEndpoint(this, StringEndpointModel, 'trainer-types') public readonly variant = new SimpleEndpoint(this, StringEndpointModel, 'variants') private lang: SupportedLanguages = 'en' private endpointURL = 'https://api.tcgdex.net/v2' public constructor(lang: SupportedLanguages = 'en') { this.setLang(lang) } /** * @deprecated use the constructor parameter or {@link TCGdex.setLang} when in an instance */ public static setDefaultLang(lang: SupportedLanguages) { TCGdex.defaultLang = lang } /** * @deprecated use {@link TCGdex.setLang} when in an instance */ public static getDefaultLang(): SupportedLanguages { return TCGdex.defaultLang } /** * the endpoint URL * ex: `https://api.tcgdex.net/v2` * @param endpoint the url */ public setEndpoint(endpoint: string) { this.endpointURL = endpoint } public getEndpoint(): string { return this.endpointURL } /** * set the current cache methodology * @param cache the cache to use */ public setCache(cache: CacheInterface) { this.cache = cache } /** * get the current cache methodology * @param cache the cache to use */ public getCache(): CacheInterface { return this.cache } /** * the endpoint URL * ex: `https://api.tcgdex.net/v2` * @param endpoint the url */ public setCacheTTL(seconds: number) { this.cacheTTL = seconds } /** * get the current useed cache ttl in seconds * @returns the cache ttl in seconds */ public getCacheTTL(): number { return this.cacheTTL } public getLang(): SupportedLanguages { return this.lang ?? TCGdex.defaultLang ?? 'en' } public setLang(lang: SupportedLanguages) { this.lang = lang } /** * Shortcut to easily fetch a card using both it's global id and it's local ID * @param id the card global/local ID * @param set the card set name/ID (optionnal) * @returns the card object */ public async fetchCard(id: string | number, set?: string): Promise { const path = set ? ['sets', set] : ['cards'] // @ts-expect-error the base endpoint is 'sets' or 'cards' return this.fetch(...path, id) } /** * Shortcut to easily fetch cards using an optionnal set name/ID * @param set the card set name/ID (optionnal) * @returns a card list */ public async fetchCards(set?: string): Promise | undefined> { if (set) { const fSet = await this.fetch('sets', set) return fSet ? fSet.cards : undefined } return this.fetch('cards') } /** * @deprecated use `this.fetch('sets', set)` */ public async fetchSet(set: string): Promise { return this.fetch('sets', set) } /** * @deprecated use `this.fetch('series', serie)` */ public async fetchSerie(serie: string): Promise { return this.fetch('series', serie) } /** * @deprecated use `this.fetch('series')` */ public async fetchSeries(): Promise { return this.fetch('series') } /** * Shortcut to easily fetch sets using an optionnal serie name/ID * @param serie the card set name/ID (optionnal) * @returns a card list */ public async fetchSets(serie?: string): Promise { if (serie) { const fSerie = await this.fetch('series', serie) return fSerie ? fSerie.sets : undefined } return this.fetch('sets') } /** * Fetch a card using its global id * @param endpoint_0 'cards' * @param endpoint_1 {string} the card global ID */ public async fetch(...type: ['cards', string]): Promise /** * Fetch every cards in the database * @param endpoint_0 'cards' */ public async fetch(type: 'cards'): Promise | undefined> /** * Fetch a card using its local id and its set * @param endpoint_0 'sets' * @param endpoint_1 {string} the set name or ID * @param endpoint_2 {string} the card local ID */ public async fetch(...endpoint: ['sets', string, string]): Promise /** * Fetch a set * @param endpoint_0 'sets' * @param endpoint_1 {string} the set name or ID */ public async fetch(...endpoint: ['sets', string]): Promise /** * Fetch every sets * @param endpoint_0 'sets' */ public async fetch(endpoint: 'sets'): Promise /** * Fetch a serie * @param endpoint_0 'series' * @param endpoint_1 {string} the serie name or ID */ public async fetch(...endpoint: ['series', string]): Promise /** * Fetch every series * @param endpoint_0 'series' */ public async fetch(endpoint: 'series'): Promise /** * Fetch cards depending on a specific filter * @param endpoint_0 {'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants'} * Possible value 'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants' * @param endpoint_1 {string} the value set while fetching the index */ public async fetch(...endpoint: ['categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants', string]): Promise /** * Fetch cards depending on a specific filter * @param endpoint_0 {'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants'} * Possible value 'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants' * @param endpoint_1 {string} Fetch the possible values to use depending on the endpoint */ public async fetch(endpoint: 'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants'): Promise | undefined> /** * Fetch The differents endpoints depending on the first argument * @param endpoint_0 {'hp' | 'retreats' | 'categories' | 'illustrators' | 'rarities' | 'types'} * Possible value 'cards' | 'categories' | 'hp' | 'illustrators' | 'rarities' | 'retreats' | 'series' | 'sets' | 'types' * @param endpoint_1 {string} (Optionnal) some details to go from the index file to the item file (mostly the ID/name) * @param endpoint_2 {string} (Optionnal) only for sets the card local ID to fetch the card through the set */ public async fetch(...endpoint: Array): Promise { if (endpoint.length === 0) { throw new Error('endpoint to fetch is empty!') } // @ts-expect-error with the precedent check, we KNOW that type is not empty const baseEndpoint = endpoint.shift().toLowerCase() as Endpoint if (!ENDPOINTS.includes(baseEndpoint)) { throw new Error(`unknown endpoint to fetch! (${baseEndpoint})`) } return this.actualFetch(this.getFullURL([baseEndpoint, ...endpoint])) } /** * @param endpoint the endpoint to fetch * @param query the query */ public async fetchWithQuery( endpoint: [Endpoints, ...Array], query?: Array<{ key: string, value: string | number | boolean }> ): Promise { if (endpoint.length === 0) { throw new Error('endpoint to fetch is empty!') } const baseEndpoint = endpoint[0].toLowerCase() as Endpoints if (!ENDPOINTS.includes(baseEndpoint)) { throw new Error(`unknown endpoint to fetch! (${baseEndpoint})`) } return this.actualFetch(this.getFullURL(endpoint, query)) } /** * format the final URL */ private getFullURL( url: Array, searchParams?: Array<{ key: string, value: string | number | boolean }> ): string { // Normalize path let path = url.map(this.encode).join('/') // handle the Search Params if (searchParams) { path += '?' for (let idx = 0; idx < searchParams.length; idx++) { const param = searchParams[idx] if (idx !== 0) { path += '&' } path += `${this.encode(param.key)}=${this.encode(param.value)}` } } // return with the endpoint and all the shit return `${this.getEndpoint()}/${this.getLang()}/${path}` } private async actualFetch(path: string): Promise { // get and return the cached value if available const cached = this.cache.get(path) if (cached) { return cached as T } // the actual Fetch :D const resp = await TCGdex.fetch(path, { headers: { 'user-agent': `@tcgdex/javascript-sdk/${version}` } }) // throw if a server-side error is occured if (resp.status >= 500) { try { const json = JSON.stringify(await resp.json()) throw new Error(json) } catch { throw new Error('TCGdex Server responded with an invalid error :(') } } // response is not valid :O if (resp.status !== 200) { return undefined } // parse, put to cache and return const json = await resp.json() this.cache.set(path, json, this.cacheTTL) return json as T } /** * encode a string to be used in an url * @param str the string to encode to URL * @returns the encoded string */ private encode(str: string | number | boolean): string { return encodeURI( str // Transform numbers to string .toString() // replace this special character with an escaped one .replace('?', '%3F') // normalize the string .normalize('NFC') // remove some special chars // eslint-disable-next-line no-misleading-character-class .replace(/["'\u0300-\u036f]/gu, '') ) } } export * from './models/Card' export { Query }