Files
javascript-sdk/src/tcgdex.ts

389 lines
12 KiB
TypeScript

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<Card | undefined> {
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<Array<CardResume> | 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<TCGdexSet | undefined> {
return this.fetch('sets', set)
}
/**
* @deprecated use `this.fetch('series', serie)`
*/
public async fetchSerie(serie: string): Promise<Serie | undefined> {
return this.fetch('series', serie)
}
/**
* @deprecated use `this.fetch('series')`
*/
public async fetchSeries(): Promise<SerieList | undefined> {
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<SetList | undefined> {
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<Card | undefined>
/**
* Fetch every cards in the database
* @param endpoint_0 'cards'
*/
public async fetch(type: 'cards'): Promise<Array<CardResume> | 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<Card | undefined>
/**
* Fetch a set
* @param endpoint_0 'sets'
* @param endpoint_1 {string} the set name or ID
*/
public async fetch(...endpoint: ['sets', string]): Promise<TCGdexSet | undefined>
/**
* Fetch every sets
* @param endpoint_0 'sets'
*/
public async fetch(endpoint: 'sets'): Promise<SetList | undefined>
/**
* Fetch a serie
* @param endpoint_0 'series'
* @param endpoint_1 {string} the serie name or ID
*/
public async fetch(...endpoint: ['series', string]): Promise<Serie | undefined>
/**
* Fetch every series
* @param endpoint_0 'series'
*/
public async fetch(endpoint: 'series'): Promise<SerieList | undefined>
/**
* 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<StringEndpoint | undefined>
/**
* 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<Array<string> | 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<T = object>(...endpoint: Array<Endpoints | string>): Promise<T | undefined> {
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<T>(this.getFullURL([baseEndpoint, ...endpoint]))
}
/**
* @param endpoint the endpoint to fetch
* @param query the query
*/
public async fetchWithQuery<T = object>(
endpoint: [Endpoints, ...Array<string>],
query?: Array<{ key: string, value: string | number | boolean }>
): Promise<T | undefined> {
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<T>(this.getFullURL(endpoint, query))
}
/**
* format the final URL
*/
private getFullURL(
url: Array<string | number>,
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<T = object>(path: string): Promise<T | undefined> {
// 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
}