feat: Allow a user to filters elements on the API (#275)

This commit is contained in:
Florian Bouillon 2024-10-10 14:56:11 +02:00 committed by GitHub
parent f2621890e1
commit bf54ab3809
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1429 additions and 126 deletions

View File

@ -17,7 +17,7 @@
<img src="https://img.shields.io/github/stars/tcgdex/javascript-sdk?style=flat-square" alt="Github stars"> <img src="https://img.shields.io/github/stars/tcgdex/javascript-sdk?style=flat-square" alt="Github stars">
</a> </a>
<a href="https://github.com/tcgdex/javascript-sdk/actions/workflows/build.yml"> <a href="https://github.com/tcgdex/javascript-sdk/actions/workflows/build.yml">
<img src="https://img.shields.io/github/workflow/status/tcgdex/javascript-sdk/Build%20&%20Test?style=flat-square" alt="the TCGdex JAvascript SDK is released under the MIT license." /> <img src="https://img.shields.io/github/actions/workflow/status/tcgdex/javascript-sdk/build.yml?style=flat-square" alt="the TCGdex JAvascript SDK is released under the MIT license." />
</a> </a>
<a href="https://discord.gg/NehYTAhsZE"> <a href="https://discord.gg/NehYTAhsZE">
<img src="https://img.shields.io/discord/857231041261076491?color=%235865F2&label=Discord&style=flat-square" alt="Discord Link"> <img src="https://img.shields.io/discord/857231041261076491?color=%235865F2&label=Discord&style=flat-square" alt="Discord Link">

View File

@ -1,5 +1,7 @@
const TCGdex = require("../src/tcgdex").default /// <reference types="jest" />
const fetch = require('node-fetch')
const { default: TCGdex, Query } = require("../src/tcgdex")
import fetch from 'node-fetch'
const fakeFetch = (response, status = 200) => jest.fn(() => const fakeFetch = (response, status = 200) => jest.fn(() =>
Promise.resolve({ Promise.resolve({
@ -8,8 +10,6 @@ const fakeFetch = (response, status = 200) => jest.fn(() =>
}) })
); );
test('Basic test', async () => { test('Basic test', async () => {
const tcgdex = new TCGdex('en') const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ ok: true }) TCGdex.fetch = fakeFetch({ ok: true })
@ -18,16 +18,6 @@ test('Basic test', async () => {
expect(TCGdex.fetch).toHaveBeenCalledTimes(1) expect(TCGdex.fetch).toHaveBeenCalledTimes(1)
}) })
test('Cache test', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ok: 'a'})
const res1 = await tcgdex.fetch('cards', 'cache-test')
expect(res1).toEqual({ok: 'a'})
TCGdex.fetch = fakeFetch({ok: 'b'})
const res2 = await tcgdex.fetch('cards', 'cache-test')
expect(res2).toEqual({ok: 'a'})
})
test('endpoint errors', async () => { test('endpoint errors', async () => {
const tcgdex = new TCGdex('en') const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ ok: 'a' }) TCGdex.fetch = fakeFetch({ ok: 'a' })
@ -35,32 +25,116 @@ test('endpoint errors', async () => {
await expect(tcgdex.fetch()).rejects.toThrow() await expect(tcgdex.fetch()).rejects.toThrow()
}) })
test('404 test', async () => { test(`404 error`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch(undefined, 404)
expect(
await tcgdex.fetch('cards', '404-test')
).not.toBeDefined()
})
test('test real endpoints', async () => {
const tcgdex = new TCGdex('en') const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch TCGdex.fetch = fetch
expect(
await tcgdex.card.get('404-error')
).toBeNull()
})
test(`test getting full set from list`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (await tcgdex.set.list())[0].getSet()
).toBeTruthy()
})
test(`test getting full serie from list`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (await tcgdex.serie.list())[0].getSerie()
).toBeTruthy()
})
test(`test getting full card from list`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (await tcgdex.card.list())[0].getCard()
).toBeTruthy()
})
test(`test get set from card`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (await tcgdex.card.get('swsh1-136')).getSet()
).toBeTruthy()
})
test(`test get serie from set`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (await tcgdex.set.get('swsh1')).getSerie()
).toBeTruthy()
})
test(`advanced query system`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
(await tcgdex.card.list(
Query.create()
.equal('name', 'Pikachu')
.greaterOrEqualThan('hp', 60)
.lesserThan('hp', 70)
.contains('localId', '5')
.not.contains('localId', 'tg')
.not.equal('id', 'cel25-5')
.sort('localId', 'ASC')
.paginate(3, 2)
)).length
).toBe(2)
})
const endpoints = [ const endpoints = [
{endpoint: 'fetchCard', params: ['swsh1-1']}, { endpoint: 'card', params: ['swsh1-136'] },
{endpoint: 'fetchCard', params: ['1', 'Sword & Shield']}, { endpoint: 'set', params: ['swsh1'] },
{endpoint: 'fetchCards', params: ['swsh1']}, { endpoint: 'serie', params: ['swsh'] },
{endpoint: 'fetchCards', params: []}, { endpoint: 'type', params: ['fire'] },
{endpoint: 'fetchSet', params: ['swsh1']}, { endpoint: 'retreat', params: ['1'] },
{endpoint: 'fetchSets', params: ['swsh']}, { endpoint: 'rarity', params: ['common'] },
{endpoint: 'fetchSets', params: []}, { endpoint: 'illustrator', params: [''] },
{endpoint: 'fetchSeries', params: []}, { endpoint: 'hp', params: ['30'] },
{endpoint: 'fetchSerie', params: ['swsh']}, { endpoint: 'categorie', params: ['pokemon'] },
{ endpoint: 'dexID', params: ['1'] },
{ endpoint: 'energyType', params: ['normal'] },
{ endpoint: 'regulationMark', params: ['f'] },
{ endpoint: 'stage', params: ['basic'] },
{ endpoint: 'suffixe', params: ['ex'] },
{ endpoint: 'trainerType', params: ['item'] },
{ endpoint: 'variant', params: ['normal'] },
] ]
for await (const item of endpoints) { for (const endpoint of endpoints) {
test(`test real ${endpoint.endpoint} endpoint list`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect( expect(
await tcgdex[item.endpoint](...item.params) await (tcgdex[endpoint.endpoint]).list()
).toBeDefined() ).toBeTruthy()
}
}) })
test(`test real ${endpoint.endpoint} endpoint item`, async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
expect(
await (tcgdex[endpoint.endpoint]).get(endpoint.params[0])
).toBeTruthy()
})
}

54
__tests__/cache.test.js Normal file
View File

@ -0,0 +1,54 @@
/// <reference types="jest" />
const { default: MemoryCache } = require("../src/Psr/SimpleCache/MemoryCache")
const TCGdex = require("../src/tcgdex").default
test('that cache store and get one element', async () => {
const cache = new MemoryCache()
cache.set('a', 'b')
expect(cache.get('a')).toBe('b')
})
test('that cache store and get multiple elements', async () => {
const cache = new MemoryCache()
cache.setMultiple({
'a': 'b',
'c': 'd'
})
expect(cache.getMultiple(['a', 'c'])).toStrictEqual({
a: 'b',
c: 'd'
})
})
test('cache expiration', async () => {
const cache = new MemoryCache()
cache.set('a', 'b', 1)
// wait 2 secs
await new Promise((res) => setTimeout(res, 2000))
expect(cache.get('a')).toBeUndefined()
})
test('cache deletion', async () => {
const cache = new MemoryCache()
cache.set('a', 'b')
expect(cache.get('a')).toBe('b')
cache.delete('a')
expect(cache.get('a')).toBeUndefined()
})
test('cache cleared', async () => {
const cache = new MemoryCache()
cache.set('a', 'b')
expect(cache.get('a')).toBe('b')
cache.clear()
expect(cache.get('a')).toBeUndefined()
})
test('cache exists', async () => {
const cache = new MemoryCache()
expect(cache.has('a')).toBe(false)
cache.set('a', 'b')
expect(cache.has('a')).toBe(true)
})

View File

@ -0,0 +1,66 @@
const TCGdex = require("../src/tcgdex").default
const fetch = require('node-fetch')
const fakeFetch = (response, status = 200) => jest.fn(() =>
Promise.resolve({
status: status,
json: () => Promise.resolve(response),
})
);
test('Basic test', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ok: true})
const res = await tcgdex.fetch('cards', 'basic-test')
expect(res).toEqual({ok: true})
expect(TCGdex.fetch).toHaveBeenCalledTimes(1)
})
test('Cache test', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ok: 'a'})
const res1 = await tcgdex.fetch('cards', 'cache-test')
expect(res1).toEqual({ok: 'a'})
TCGdex.fetch = fakeFetch({ok: 'b'})
const res2 = await tcgdex.fetch('cards', 'cache-test')
expect(res2).toEqual({ok: 'a'})
})
test('endpoint errors', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch({ok: 'a'})
await expect(tcgdex.fetch('non existing endpoint')).rejects.toThrow()
await expect(tcgdex.fetch()).rejects.toThrow()
})
test('404 test', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fakeFetch(undefined, 404)
expect(
await tcgdex.fetch('cards', '404-test')
).not.toBeDefined()
})
test('test real endpoints', async () => {
const tcgdex = new TCGdex('en')
TCGdex.fetch = fetch
const endpoints = [
{endpoint: 'fetchCard', params: ['swsh1-1']},
{endpoint: 'fetchCard', params: ['1', 'Sword & Shield']},
{endpoint: 'fetchCards', params: ['swsh1']},
{endpoint: 'fetchCards', params: []},
{endpoint: 'fetchSet', params: ['swsh1']},
{endpoint: 'fetchSets', params: ['swsh']},
{endpoint: 'fetchSets', params: []},
{endpoint: 'fetchSeries', params: []},
{endpoint: 'fetchSerie', params: ['swsh']},
]
for await (const item of endpoints) {
expect(
await tcgdex[item.endpoint](...item.params)
).toBeDefined()
}
})

58
package-lock.json generated
View File

@ -1,16 +1,16 @@
{ {
"name": "@tcgdex/sdk", "name": "@tcgdex/sdk",
"version": "2.5.0", "version": "2.6.0-beta.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@tcgdex/sdk", "name": "@tcgdex/sdk",
"version": "2.5.0", "version": "2.6.0-beta.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"isomorphic-unfetch": "^3", "@dzeio/object-util": "^1",
"unfetch": "^4" "isomorphic-unfetch": "^3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7", "@babel/core": "^7",
@ -1750,11 +1750,9 @@
} }
}, },
"node_modules/@dzeio/object-util": { "node_modules/@dzeio/object-util": {
"version": "1.4.5", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.8.3.tgz",
"integrity": "sha512-V04GE77lipF2qnzMuA+T3blyPVo+ABKQLrmEteerXecA7G+TCisyQKIVMewFvF9qNsJ1LOVTckWW9wnRPyAwoQ==", "integrity": "sha512-/d0ezut7EGrEKedcD8K2Jb2NAMSFfhxNj4rpUBlGzmmakJjJCXAgXvSDLjUwYrgHuabxbxlAn90Wo727MCzWLA=="
"dev": true,
"peer": true
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "1.4.1", "version": "1.4.1",
@ -2768,10 +2766,13 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.11.18", "version": "20.12.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
"dev": true "dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
}, },
"node_modules/@types/node-fetch": { "node_modules/@types/node-fetch": {
"version": "2.6.2", "version": "2.6.2",
@ -8889,6 +8890,12 @@
"typpy": "^2.3.4" "typpy": "^2.3.4"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/unfetch": { "node_modules/unfetch": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
@ -10387,11 +10394,9 @@
} }
}, },
"@dzeio/object-util": { "@dzeio/object-util": {
"version": "1.4.5", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.4.5.tgz", "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.8.3.tgz",
"integrity": "sha512-V04GE77lipF2qnzMuA+T3blyPVo+ABKQLrmEteerXecA7G+TCisyQKIVMewFvF9qNsJ1LOVTckWW9wnRPyAwoQ==", "integrity": "sha512-/d0ezut7EGrEKedcD8K2Jb2NAMSFfhxNj4rpUBlGzmmakJjJCXAgXvSDLjUwYrgHuabxbxlAn90Wo727MCzWLA=="
"dev": true,
"peer": true
}, },
"@eslint/eslintrc": { "@eslint/eslintrc": {
"version": "1.4.1", "version": "1.4.1",
@ -11183,10 +11188,13 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "18.11.18", "version": "20.12.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
"dev": true "dev": true,
"requires": {
"undici-types": "~5.26.4"
}
}, },
"@types/node-fetch": { "@types/node-fetch": {
"version": "2.6.2", "version": "2.6.2",
@ -15647,6 +15655,12 @@
"typpy": "^2.3.4" "typpy": "^2.3.4"
} }
}, },
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"unfetch": { "unfetch": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "@tcgdex/sdk", "name": "@tcgdex/sdk",
"version": "2.5.1", "version": "2.6.0-beta.1",
"main": "./dist/tcgdex.node.js", "main": "./dist/tcgdex.node.js",
"module": "./dist/tcgdex.node.mjs", "module": "./dist/tcgdex.node.mjs",
"types": "./dist/tcgdex.node.d.ts", "types": "./dist/tcgdex.node.d.ts",
@ -53,8 +53,8 @@
"node": ">=12" "node": ">=12"
}, },
"dependencies": { "dependencies": {
"isomorphic-unfetch": "^3", "@dzeio/object-util": "^1",
"unfetch": "^4" "isomorphic-unfetch": "^3"
}, },
"scripts": { "scripts": {
"prebuild": "node scripts/export-version-number.js", "prebuild": "node scripts/export-version-number.js",

View File

@ -0,0 +1,38 @@
import { objectLoop } from '@dzeio/object-util'
import type CacheInterface from './CacheInterface'
export default abstract class CacheAsbract implements CacheInterface {
public getMultiple<T>(keys: Array<string>, defaultValues?: Array<T> | undefined): Record<string, T> {
const res: Record<string, T> = {}
for (let idx = 0; idx < keys.length; idx++) {
const key = keys[idx] as string
const value = this.get(key, defaultValues?.[idx]) as T | undefined
if (typeof value === 'undefined') {
continue
}
res[key] = value
}
return res
}
public setMultiple<T>(values: Record<string, T>, ttl?: number | undefined): boolean {
objectLoop(values, (v, k) => {
this.set(k, v, ttl)
})
return true
}
public deleteMultiple(keys: Array<string>): boolean {
for (const key of keys) {
this.delete(key)
}
return true
}
public abstract get<T>(key: string, defaultValue?: T): T | undefined
public abstract set<T>(key: string, value: T, ttl?: number): boolean
public abstract delete(key: string): boolean
public abstract clear(): boolean
public abstract has(key: string): boolean
}

109
src/Psr/SimpleCache/CacheInterface.d.ts vendored Normal file
View File

@ -0,0 +1,109 @@
export default interface CacheInterface {
/**
* Fetches a value from the cache.
*
* @param key The unique key of this item in the cache.
* @param defaultValue Default value to return if the key does not exist.
*
* @return T The value of the item from the cache, or $default in case of cache miss.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if the $key string is not a legal value.
*/
get<T>(key: string, defaultValue?: T): T | undefined
/**
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
*
* @param key The key of the item to store.
* @param value The value of the item to store. Must be serializable.
* @param {null|number} ttl The TTL value of this item. If no value is sent and
* the driver supports TTL then the library may set a default value
* for it or let the driver take care of that.
*
* @return bool True on success and false on failure.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if the $key string is not a legal value.
*/
set<T>(key: string, value: T, ttl?: number): boolean
/**
* Delete an item from the cache by its unique key.
*
* @param key The unique cache key of the item to delete.
*
* @return True if the item was successfully removed. False if there was an error.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if the $key string is not a legal value.
*/
delete(key: string): boolean
/**
* Wipes clean the entire cache's keys.
*
* @return boolean True on success and false on failure.
*/
clear(): boolean
/**
* Obtains multiple cache items by their unique keys.
*
* @param keys A list of keys that can obtained in a single operation.
* @param defaultValues $default Default value to return for keys that do not exist.
*
* @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if $keys is neither an array nor a Traversable,
* or if any of the $keys are not a legal value.
*/
getMultiple<T>(keys: Array<string>, defaultValues?: Array<T>): Record<string, T>
/**
* Persists a set of key => value pairs in the cache, with an optional TTL.
*
* @param values A list of key => value pairs for a multiple-set operation.
* @param ttl Optional. The TTL value of this item. If no value is sent and
* the driver supports TTL then the library may set a default value
* for it or let the driver take care of that.
*
* @return bool True on success and false on failure.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if $values is neither an array nor a Traversable,
* or if any of the $values are not a legal value.
*/
setMultiple<T>(values: Record<string, T>, ttl?: number): boolean
/**
* Deletes multiple cache items in a single operation.
*
* @param keys A list of string-based keys to be deleted.
*
* @return bool True if the items were successfully removed. False if there was an error.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if $keys is neither an array nor a Traversable,
* or if any of the $keys are not a legal value.
*/
deleteMultiple(keys: Array<string>): boolean
/**
* Determines whether an item is present in the cache.
*
* NOTE: It is recommended that has() is only to be used for cache warming type purposes
* and not to be used within your live applications operations for get/set, as this method
* is subject to a race condition where your has() will return true and immediately after,
* another script can remove it, making the state of your app out of date.
*
* @param key The cache item key.
*
* @return bool
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if the $key string is not a legal value.
*/
has(key: string): boolean
}

View File

@ -0,0 +1,91 @@
import CacheAsbract from './CacheAbstract'
interface CacheItem<T> {
data: T
expire?: number | undefined
}
/**
* A cache implementation that uses browser storage.
*
* This class extends `CacheAsbract` and provides a concrete implementation
* of the caching interface. It stores cached items in browser storage,
* which is suitable for storing small amounts of data.
*/
export default class BrowserStorageCache extends CacheAsbract {
private storage: Storage
public constructor(private readonly prefix?: string, session = false) {
super()
if (session) {
this.storage = window.sessionStorage
} else {
this.storage = window.localStorage
}
}
public get<T>(key: string, defaultValue?: T | undefined): T | undefined {
const raw = this.storage.getItem(this.getFinalKey(key))
if (!raw) {
return defaultValue ?? undefined
}
const item: CacheItem<T> = JSON.parse(raw)
if (item.expire && item.expire < new Date().getTime()) {
this.delete(key)
return defaultValue ?? undefined
}
return item.data
}
public set<T>(key: string, value: T, ttl?: number | undefined): boolean {
let expire = undefined
if (ttl) {
expire = (new Date()).getTime() + (ttl * 1000)
}
const data: CacheItem<unknown> = {
data: value,
expire: expire
}
this.storage.setItem(this.getFinalKey(key), JSON.stringify(data))
return true
}
public delete(key: string): boolean {
this.storage.removeItem(this.getFinalKey(key))
return true
}
public clear(): boolean {
const keys = this.keys()
return this.deleteMultiple(keys)
}
public has(key: string): boolean {
return !!this.storage.getItem(this.getFinalKey(key))
}
private keys(): Array<string> {
const list: Array<string> = []
for (let idx = 0; idx < this.storage.length; idx++) {
const key = this.storage.key(idx)
if (!key || this.prefix && !key?.startsWith(`${this.prefix}/`)) {
continue
}
list.push(key)
}
return list
}
private getFinalKey(key: string): string {
if (!this.prefix) {
return key
}
return `${this.prefix}/${key}`
}
}

View File

@ -0,0 +1,58 @@
import CacheAsbract from './CacheAbstract'
interface CacheItem<T> {
data: T
expire?: number | undefined
}
/**
* Memory cache implementation that stores cached items in memory.
* This class extends the abstract `CacheAbstract` and provides a basic in-memory caching mechanism.
*
* @class MemoryCache
*/
export default class MemoryCache extends CacheAsbract {
private cache: Map<string, CacheItem<unknown>> = new Map()
public get<T>(key: string, defaultValue?: T | undefined): T | undefined {
const item = this.cache.get(key)
if (!item) {
return defaultValue ?? undefined
}
if (item.expire && item.expire < new Date().getTime()) {
this.delete(key)
return defaultValue ?? undefined
}
return item.data as T | undefined
}
public set<T>(key: string, value: T, ttl?: number | undefined): boolean {
let expire: number | undefined
if (ttl) {
expire = new Date().getTime() + ttl * 1000
}
this.cache.set(key, {
data: value,
expire: expire
})
return true
}
public delete(key: string): boolean {
this.cache.delete(key)
return true
}
public clear(): boolean {
this.cache.clear()
return true
}
public has(key: string): boolean {
return this.cache.has(key)
}
}

88
src/Query.ts Normal file
View File

@ -0,0 +1,88 @@
export default class Query {
public params: Array<{ key: string, value: string | number | boolean }> = []
public not: {
equal: (key: string, value: string) => Query
contains: (key: string, value: string) => Query
includes: (key: string, value: string) => Query
like: (key: string, value: string) => Query
isNull: (key: string) => Query
} = {
equal: (key: string, value: string) => {
this.params.push({ key: key, value: `neq:${value}` })
return this
},
contains: (key: string, value: string) => {
this.params.push({ key: key, value: `not:${value}` })
return this
},
includes: (key: string, value: string) => this.not.contains(key, value),
like: (key: string, value: string) => this.not.contains(key, value),
isNull: (key: string) => {
this.params.push({ key: key, value: 'notnull:' })
return this
}
}
public static create(): Query {
return new Query()
}
public includes(key: string, value: string): this {
return this.contains(key, value)
}
public like(key: string, value: string): this {
return this.contains(key, value)
}
public contains(key: string, value: string): this {
this.params.push({ key: key, value: value })
return this
}
public equal(key: string, value: string): this {
this.params.push({ key: key, value: `eq:${value}` })
return this
}
public sort(key: string, order: 'ASC' | 'DESC'): this {
this.params.push({ key: 'sort:field', value: key })
this.params.push({ key: 'sort:order', value: order })
return this
}
public greaterOrEqualThan(key: string, value: number) {
this.params.push({ key: key, value: `gte:${value}` })
return this
}
public lesserOrEqualThan(key: string, value: number) {
this.params.push({ key: key, value: `lte:${value}` })
return this
}
public greaterThan(key: string, value: number) {
this.params.push({ key: key, value: `gt:${value}` })
return this
}
public lesserThan(key: string, value: number) {
this.params.push({ key: key, value: `lt:${value}` })
return this
}
public isNull(key: string) {
this.params.push({ key: key, value: 'null:' })
return this
}
public paginate(page: number, itemsPerPage: number): this {
this.params.push({ key: 'pagination:page', value: page })
this.params.push({ key: 'pagination:itemsPerPage', value: itemsPerPage })
return this
}
}

View File

@ -1,31 +0,0 @@
import TCGdex from './tcgdex'
import { version } from './version'
export default class Request {
// 1 hour of TTL by default
public static ttl = 1000 * 60 * 60
private static cache: Record<string, {response: any, time: number}> = {}
public static async fetch<T>(url: string): Promise<T | undefined> {
let request = this.cache[url]
const now = new Date().getTime()
if (!request || now - request.time > this.ttl) {
const unfetch = TCGdex.fetch
const resp = await unfetch(url, {
headers: {
'user-agent': `@tcgdex/javascript-sdk/${version}`
}
})
if (resp.status !== 200) {
return undefined
}
this.cache[url] = { response: await resp.json(), time: now }
request = this.cache[url]
}
return request.response
}
}

26
src/endpoints/Endpoint.ts Normal file
View File

@ -0,0 +1,26 @@
import type { Endpoints } from '../interfaces'
import Model from '../models/Model'
import type Query from '../Query'
import type TCGdex from '../tcgdex'
export default class Endpoint<Item extends Model, List extends Model> {
public constructor(
protected readonly tcgdex: TCGdex,
protected readonly itemModel: new (sdk: TCGdex) => Item,
protected readonly listModel: new (sdk: TCGdex) => List,
protected readonly endpoint: Endpoints
) { }
public async get(id: string | number): Promise<Item | null> {
const res = await this.tcgdex.fetch(this.endpoint as 'cards', id as string)
if (!res) {
return null
}
return Model.build(new this.itemModel(this.tcgdex), res)
}
public async list(query?: Query): Promise<Array<List>> {
const res = await this.tcgdex.fetchWithQuery([this.endpoint], query?.params)
return (res as Array<object> ?? []).map((it) => Model.build(new this.listModel(this.tcgdex), it))
}
}

View File

@ -0,0 +1,24 @@
import type { Endpoints } from '../interfaces'
import Model from '../models/Model'
import type Query from '../Query'
import type TCGdex from '../tcgdex'
export default class SimpleEndpoint<Item extends Model, List extends string | number> {
public constructor(
protected readonly tcgdex: TCGdex,
protected readonly itemModel: new (sdk: TCGdex) => Item,
protected readonly endpoint: Endpoints
) {}
public async get(id: string | number): Promise<Item | null> {
const res = await this.tcgdex.fetch(this.endpoint as 'cards', id as string)
if (!res) {
return null
}
return Model.build(new this.itemModel(this.tcgdex), res)
}
public async list(query?: Query): Promise<Array<List>> {
return await this.tcgdex.fetchWithQuery([this.endpoint], query?.params) ?? []
}
}

View File

@ -310,3 +310,12 @@ export interface StringEndpoint {
name: string name: string
cards: Array<CardResume> cards: Array<CardResume>
} }
export type Quality = 'low' | 'high'
export type Extension = 'jpg' | 'webp' | 'png'
export type Endpoints = 'cards' | 'categories' | 'dex-ids' | 'energy-types' |
'hp' | 'illustrators' | 'rarities' | 'regulation-marks' |
'retreats' | 'series' | 'sets' | 'stages' | 'suffixes' |
'trainer-types' | 'types' | 'variants'

198
src/models/Card.ts Normal file
View File

@ -0,0 +1,198 @@
import CardResume from './CardResume'
import type { Variants } from './Other'
import type TCGdexSet from './Set'
import type SetResume from './SetResume'
// TODO: sort elements by alphabetical order
export default class Card extends CardResume {
/**
* Card illustrator
*/
public illustrator?: string
/**
* Card Rarity
*
* - None https://www.tcgdex.net/database/sm/smp/SM01
* - Common https://www.tcgdex.net/database/xy/xy9/1
* - Uncommon https://www.tcgdex.net/database/xy/xy9/2
* - Rare https://www.tcgdex.net/database/xy/xy9/3
* - Ultra Rare
* - Secret Rare
*/
public rarity!: string
/**
* Card Category
*
* - Pokemon
* - Trainer
* - Energy
*/
public category!: string
/**
* Card Variants (Override Set Variants)
*/
public variants?: Variants
/**
* Card Set
*/
public set!: SetResume
/**
* Pokemon only elements
*/
/**
* Pokemon Pokedex ID
*/
public dexId?: Array<number>
/**
* Pokemon HP
*/
public hp?: number
/**
* Pokemon Types
* ex for multiple https://www.tcgdex.net/database/ex/ex13/17
*/
public types?: Array<string>
/**
* Pokemon Sub Evolution
*/
public evolveFrom?: string
/**
* Pokemon Weight
*/
public weight?: string
/**
* Pokemon Description
*/
public description?: string
/**
* Level of the Pokemon
*
* NOTE: can be equal to 'X' when the pokemon is a LEVEL-UP one
*/
public level?: number | string
/**
* Pokemon Stage
*
* - Basic https://www.tcgdex.net/database/xy/xy9/1
* - BREAK https://www.tcgdex.net/database/xy/xy9/18
* - LEVEL-UP https://www.tcgdex.net/database/dp/dp1/121
* - MEGA https://www.tcgdex.net/database/xy/xy1/2
* - RESTORED https://www.tcgdex.net/database/bw/bw5/53
* - Stage1 https://www.tcgdex.net/database/xy/xy9/2
* - Stage2 https://www.tcgdex.net/database/xy/xy9/3
* - VMAX https://www.tcgdex.net/database/swsh/swsh1/50
*/
public stage?: string
/**
* Card Suffix
*
* - EX https://www.tcgdex.net/database/ex/ex2/94
* - GX https://www.tcgdex.net/database/sm/sm12/4
* - V https://www.tcgdex.net/database/swsh/swsh1/1
* - Legend https://www.tcgdex.net/database/hgss/hgss1/114
* - Prime https://www.tcgdex.net/database/hgss/hgss2/85
* - SP https://www.tcgdex.net/database/pl/pl1/7
* - TAG TEAM-GX https://www.tcgdex.net/database/sm/sm12/226
*/
public suffix?: string
/**
* Pokemon Held Item
*
* ex https://www.tcgdex.net/database/dp/dp2/75
*/
public item?: {
name: string
effect: string
}
/**
* Pokemon Abilities
*
* multi abilities ex https://www.tcgdex.net/database/ex/ex15/10
*/
public abilities?: Array<{
type: string
name: string
effect: string
}>
/**
* Pokemon Attacks
*/
public attacks?: Array<{
cost?: Array<string>
name: string
effect?: string
damage?: string | number
}>
/**
* Pokemon Weaknesses
*/
public weaknesses?: Array<{
type: string
value?: string
}>
public resistances?: Array<{
type: string
value?: string
}>
public retreat?: number
// Trainer/Energy
public effect?: string
// Trainer Only
public trainerType?: string
// Energy Only
public energyType?: string
/**
* Define the rotation mark on cards >= Sword & Shield
*/
public regulationMark?: string
/**
* Card ability to be played in official tournaments
*
* Note: all cards are avaialable to play in unlimited tournaments
*/
public legal!: {
/**
* Ability to play in standard tournaments
*/
standard: boolean
/**
* Ability to play in expanded tournaments
*/
expanded: boolean
}
public override async getCard(): Promise<Card> {
return this
}
public async getSet(): Promise<TCGdexSet> {
return (await this.sdk.set.get(this.set.id))!
}
}

47
src/models/CardResume.ts Normal file
View File

@ -0,0 +1,47 @@
import type { Extension, Quality } from '../interfaces'
import type Card from './Card'
import Model from './Model'
export default class CardResume extends Model {
/**
* Globally unique card ID based on the set ID and the cards ID within the set
*/
public id!: string
/**
* Card image url without the extension and quality
*
* @see {@link getImageURL}
*/
public image?: string
/**
* ID indexing this card within its set, usually just its number
*/
public localId!: string
/**
* Card Name (Including the suffix if next to card name)
*/
public name!: string
/**
* the the Card Image full URL
*
* @param {Quality} quality the quality you want your image to be in
* @param {Extension} extension extension you want you image to be
* @return the full card URL
*/
public getImageURL(quality: Quality = 'high', extension: Extension = 'png'): string {
return `${this.image}/${quality}.${extension}`
}
/**
* Get the full Card
*
* @return the full card if available
*/
public async getCard(): Promise<Card> {
return (await this.sdk.card.get(this.id))!
}
}

28
src/models/Model.ts Normal file
View File

@ -0,0 +1,28 @@
import { objectLoop } from '@dzeio/object-util'
import type TCGdex from '../tcgdex'
export default abstract class Model {
public constructor(
protected readonly sdk: TCGdex
) { }
/**
* build a model depending on the data given
* @param model the model to build
* @param data the data to fill it with
*/
public static build<T extends Model>(model: T, data?: object): T {
if (!data) {
throw new Error('data is necessary.')
}
model.fill(data)
return model
}
protected fill(obj: object) {
objectLoop(obj, (value, key) => {
(this as object)[key] = value
})
}
}

6
src/models/Other.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export interface Variants {
normal?: boolean
reverse?: boolean
holo?: boolean
firstEdition?: boolean
}

21
src/models/Serie.ts Normal file
View File

@ -0,0 +1,21 @@
import { objectLoop } from '@dzeio/object-util'
import Model from './Model'
import SerieResume from './SerieResume'
import SetResume from './SetResume'
export default class Serie extends SerieResume {
public sets!: Array<SetResume>
protected fill(obj: object): void {
objectLoop(obj, (value, key) => {
switch (key) {
case 'sets':
this.sets = (value as Array<any>).map((it) => Model.build(new SetResume(this.sdk), it))
break
default:
this[key] = value
break
}
})
}
}

24
src/models/SerieResume.ts Normal file
View File

@ -0,0 +1,24 @@
import type { Extension } from '../interfaces'
import Model from './Model'
import type Serie from './Serie'
export default class SerieResume extends Model {
public id!: string
public name!: string
public logo?: string
/**
* the the Card Image full URL
*
* @param {Quality} quality the quality you want your image to be in
* @param {Extension} extension extension you want you image to be
* @return the full card URL
*/
public getImageURL(extension: Extension = 'png'): string {
return `${this.logo}.${extension}`
}
public async getSerie(): Promise<Serie> {
return (await this.sdk.serie.get(this.id))!
}
}

89
src/models/Set.ts Normal file
View File

@ -0,0 +1,89 @@
import { objectLoop } from '@dzeio/object-util'
import CardResume from './CardResume'
import Model from './Model'
import type { Variants } from './Other'
import type SerieResume from './SerieResume'
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
export default class Set extends Model {
public id!: string
public name!: string
public logo?: string
public symbol?: string
public serie!: SerieResume
public tcgOnline?: string
public variants?: Variants
public releaseDate!: string
/**
* Designate if the set is usable in tournaments
*
* Note: this is specific to the set and if a
* card is banned from the set it will still be true
*/
public legal!: {
/**
* Ability to play in standard tournaments
*/
standard: boolean
/**
* Ability to play in expanded tournaments
*/
expanded: boolean
}
public cardCount!: {
/**
* total of number of cards
*/
total: number
/**
* number of cards officialy (on the bottom of each cards)
*/
official: number
/**
* number of cards having a normal version
*/
normal: number
/**
* number of cards having an reverse version
*/
reverse: number
/**
* number of cards having an holo version
*/
holo: number
/**
* Number of possible cards
*/
firstEd?: number
}
public cards!: Array<CardResume>
public async getSerie() {
return this.sdk.serie.get(this.serie.id)
}
protected fill(obj: object): void {
objectLoop(obj, (value, key) => {
switch (key) {
case 'cards':
this.cards = (value as Array<any>).map((it) => Model.build(new CardResume(this.sdk), it))
break
default:
this[key] = value
break
}
})
}
}

25
src/models/SetResume.ts Normal file
View File

@ -0,0 +1,25 @@
import Model from './Model'
import type TCGdexSet from './Set'
export default class SetResume extends Model {
public id!: string
public name!: string
public logo?: string
public symbol?: string
public cardCount!: {
/**
* total of number of cards
*/
total: number
/**
* number of cards officialy (on the bottom of each cards)
*/
official: number
}
public async getSet(): Promise<TCGdexSet> {
return (await this.sdk.set.get(this.id))!
}
}

View File

@ -0,0 +1,21 @@
import { objectLoop } from '@dzeio/object-util'
import CardResume from './CardResume'
import Model from './Model'
export default class StringEndpoint extends Model {
public name!: string
public cards!: Array<CardResume>
protected fill(obj: object): void {
objectLoop(obj, (value, key) => {
switch (key) {
case 'cards':
this.cards = (value as Array<any>).map((it) => Model.build(new CardResume(this.sdk), it))
break
default:
this[key] = value
break
}
})
}
}

View File

@ -1,6 +1,5 @@
import unfetch from 'unfetch'
import TCGdex from './tcgdex' import TCGdex from './tcgdex'
TCGdex.fetch = window.fetch ?? unfetch as any TCGdex.fetch = window.fetch
export default TCGdex export default TCGdex

View File

@ -1,24 +1,144 @@
import RequestWrapper from './Request' import type CacheInterface from './Psr/SimpleCache/CacheInterface'
import { Serie, Set, Card, CardResume, SerieList, SetList, SupportedLanguages, StringEndpoint } from './interfaces' import LocalStorageCache from './Psr/SimpleCache/LocalStorageCache'
type Endpoint = 'cards' | 'categories' | 'dex-ids' | 'energy-types' | 'hp' | 'illustrators' | 'rarities' | 'regulation-marks' | 'retreats' | 'series' | 'sets' | 'stages' | 'suffixes' | 'trainer-types' | 'types' | 'variants' 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'
const ENDPOINTS: Array<Endpoint> = ['cards', 'categories', 'dex-ids', 'energy-types', 'hp', 'illustrators', 'rarities', 'regulation-marks', 'retreats', 'series', 'sets', 'stages', 'suffixes', 'trainer-types', 'types', 'variants']
const BASE_URL = 'https://api.tcgdex.net/v2'
export default class TCGdex { export default class TCGdex {
public static fetch: typeof fetch /**
* How the remote data is going to be fetched
*/
public static fetch: typeof fetch = fetch
/** /**
* @deprecated to change the lang use `this.lang` * @deprecated to change the lang use {@link TCGdex.getLang} and {@link TCGdex.setLang}
*/ */
public static defaultLang: SupportedLanguages = 'en' public static defaultLang: SupportedLanguages = 'en'
public constructor(public lang?: SupportedLanguages) {} /**
* 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 { public getLang(): SupportedLanguages {
return this.lang ?? TCGdex.defaultLang ?? 'en' 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 * 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 id the card global/local ID
@ -47,7 +167,7 @@ export default class TCGdex {
/** /**
* @deprecated use `this.fetch('sets', set)` * @deprecated use `this.fetch('sets', set)`
*/ */
public async fetchSet(set: string): Promise<Set | undefined> { public async fetchSet(set: string): Promise<TCGdexSet | undefined> {
return this.fetch('sets', set) return this.fetch('sets', set)
} }
@ -104,7 +224,7 @@ export default class TCGdex {
* @param endpoint_0 'sets' * @param endpoint_0 'sets'
* @param endpoint_1 {string} the set name or ID * @param endpoint_1 {string} the set name or ID
*/ */
public async fetch(...endpoint: ['sets', string]): Promise<Set | undefined> public async fetch(...endpoint: ['sets', string]): Promise<TCGdexSet | undefined>
/** /**
* Fetch every sets * Fetch every sets
@ -148,7 +268,7 @@ export default class TCGdex {
* @param endpoint_1 {string} (Optionnal) some details to go from the index file to the item file (mostly the ID/name) * @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 * @param endpoint_2 {string} (Optionnal) only for sets the card local ID to fetch the card through the set
*/ */
public async fetch(...endpoint: Array<Endpoint | string>): Promise<any | undefined> { public async fetch<T = object>(...endpoint: Array<Endpoints | string>): Promise<T | undefined> {
if (endpoint.length === 0) { if (endpoint.length === 0) {
throw new Error('endpoint to fetch is empty!') throw new Error('endpoint to fetch is empty!')
} }
@ -157,29 +277,112 @@ export default class TCGdex {
if (!ENDPOINTS.includes(baseEndpoint)) { if (!ENDPOINTS.includes(baseEndpoint)) {
throw new Error(`unknown endpoint to fetch! (${baseEndpoint})`) throw new Error(`unknown endpoint to fetch! (${baseEndpoint})`)
} }
return this.makeRequest(baseEndpoint, ...endpoint) return this.actualFetch<T>(this.getFullURL([baseEndpoint, ...endpoint]))
} }
/** /**
* Function to make the request and normalize the whole path * @param endpoint the endpoint to fetch
* @param query the query
*/ */
private makeRequest<T = any>(...url: Array<string | number>) { 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 // Normalize path
const path = url.map((subPath) => encodeURI( let path = url.map(this.encode).join('/')
subPath
// 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 // Transform numbers to string
.toString() .toString()
// replace this special character with an escaped one // replace this special character with an escaped one
.replace('?', '%3F') .replace('?', '%3F')
// normalize the string // normalize the string
.normalize('NFC') .normalize('NFC')
// remove some special chars by nothing // remove some special chars
// eslint-disable-next-line no-misleading-character-class // eslint-disable-next-line no-misleading-character-class
.replace(/["'\u0300-\u036f]/gu, '') .replace(/["'\u0300-\u036f]/gu, '')
)).join('/') )
return RequestWrapper.fetch<T>(`${BASE_URL}/${this.getLang()}/${path}`) }
} }
export * from './models/Card'
export {
Query
} }
export * from './interfaces'

20
src/utils.ts Normal file
View File

@ -0,0 +1,20 @@
import type { Endpoints } from './interfaces'
/**
* detect the current running context ofthe program
*/
export function detectContext(): 'browser' | 'server' {
try {
const isBrowser = !!window
return isBrowser ? 'browser' : 'server'
} catch {
return 'server'
}
}
export const ENDPOINTS: Array<Endpoints> = [
'cards', 'categories', 'dex-ids', 'energy-types',
'hp', 'illustrators', 'rarities', 'regulation-marks',
'retreats', 'series', 'sets', 'stages', 'suffixes',
'trainer-types', 'types', 'variants'
] as const

View File

@ -1,8 +1,10 @@
{ {
"extends": "./node_modules/@dzeio/config/tsconfig.base", "extends": "./node_modules/@dzeio/config/tsconfig.base",
"include": ["./src/tcgdex.node.ts"],
"compilerOptions": { "compilerOptions": {
"target": "ES2015", "outDir": "dist",
"rootDir": "./src", "strictNullChecks": true
} },
"exclude": [
"__tests__"
]
} }