diff --git a/README.md b/README.md
index f640c71..fcff41a 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
-
+
diff --git a/__tests__/basic.test.js b/__tests__/basic.test.js
index 4b7fd21..e755419 100644
--- a/__tests__/basic.test.js
+++ b/__tests__/basic.test.js
@@ -1,5 +1,7 @@
-const TCGdex = require("../src/tcgdex").default
-const fetch = require('node-fetch')
+///
+
+const { default: TCGdex, Query } = require("../src/tcgdex")
+import fetch from 'node-fetch'
const fakeFetch = (response, status = 200) => jest.fn(() =>
Promise.resolve({
@@ -8,59 +10,131 @@ const fakeFetch = (response, status = 200) => jest.fn(() =>
})
);
-
-
test('Basic test', async () => {
const tcgdex = new TCGdex('en')
- TCGdex.fetch = fakeFetch({ok: true})
+ TCGdex.fetch = fakeFetch({ ok: true })
const res = await tcgdex.fetch('cards', 'basic-test')
- expect(res).toEqual({ok: true})
+ 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'})
+ TCGdex.fetch = fakeFetch({ ok: 'a' })
await expect(tcgdex.fetch('non existing endpoint')).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)
+ TCGdex.fetch = fetch
+
expect(
- await tcgdex.fetch('cards', '404-test')
- ).not.toBeDefined()
+ await tcgdex.card.get('404-error')
+ ).toBeNull()
})
-test('test real endpoints', async () => {
+test(`test getting full set from list`, 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()
- }
+
+ 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 = [
+ { endpoint: 'card', params: ['swsh1-136'] },
+ { endpoint: 'set', params: ['swsh1'] },
+ { endpoint: 'serie', params: ['swsh'] },
+ { endpoint: 'type', params: ['fire'] },
+ { endpoint: 'retreat', params: ['1'] },
+ { endpoint: 'rarity', params: ['common'] },
+ { endpoint: 'illustrator', params: [''] },
+ { endpoint: 'hp', params: ['30'] },
+ { 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 (const endpoint of endpoints) {
+ test(`test real ${endpoint.endpoint} endpoint list`, async () => {
+ const tcgdex = new TCGdex('en')
+ TCGdex.fetch = fetch
+
+ expect(
+ await (tcgdex[endpoint.endpoint]).list()
+ ).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()
+ })
+
+}
diff --git a/__tests__/cache.test.js b/__tests__/cache.test.js
new file mode 100644
index 0000000..77d69e8
--- /dev/null
+++ b/__tests__/cache.test.js
@@ -0,0 +1,54 @@
+///
+
+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)
+})
diff --git a/__tests__/deprecated.test.js b/__tests__/deprecated.test.js
new file mode 100644
index 0000000..4b7fd21
--- /dev/null
+++ b/__tests__/deprecated.test.js
@@ -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()
+ }
+})
diff --git a/package-lock.json b/package-lock.json
index 6d05fec..ab16cc3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,16 @@
{
"name": "@tcgdex/sdk",
- "version": "2.5.0",
+ "version": "2.6.0-beta.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@tcgdex/sdk",
- "version": "2.5.0",
+ "version": "2.6.0-beta.1",
"license": "MIT",
"dependencies": {
- "isomorphic-unfetch": "^3",
- "unfetch": "^4"
+ "@dzeio/object-util": "^1",
+ "isomorphic-unfetch": "^3"
},
"devDependencies": {
"@babel/core": "^7",
@@ -1750,11 +1750,9 @@
}
},
"node_modules/@dzeio/object-util": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.4.5.tgz",
- "integrity": "sha512-V04GE77lipF2qnzMuA+T3blyPVo+ABKQLrmEteerXecA7G+TCisyQKIVMewFvF9qNsJ1LOVTckWW9wnRPyAwoQ==",
- "dev": true,
- "peer": true
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.8.3.tgz",
+ "integrity": "sha512-/d0ezut7EGrEKedcD8K2Jb2NAMSFfhxNj4rpUBlGzmmakJjJCXAgXvSDLjUwYrgHuabxbxlAn90Wo727MCzWLA=="
},
"node_modules/@eslint/eslintrc": {
"version": "1.4.1",
@@ -2768,10 +2766,13 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "18.11.18",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
- "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
- "dev": true
+ "version": "20.12.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
+ "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/node-fetch": {
"version": "2.6.2",
@@ -8889,6 +8890,12 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
@@ -10387,11 +10394,9 @@
}
},
"@dzeio/object-util": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.4.5.tgz",
- "integrity": "sha512-V04GE77lipF2qnzMuA+T3blyPVo+ABKQLrmEteerXecA7G+TCisyQKIVMewFvF9qNsJ1LOVTckWW9wnRPyAwoQ==",
- "dev": true,
- "peer": true
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@dzeio/object-util/-/object-util-1.8.3.tgz",
+ "integrity": "sha512-/d0ezut7EGrEKedcD8K2Jb2NAMSFfhxNj4rpUBlGzmmakJjJCXAgXvSDLjUwYrgHuabxbxlAn90Wo727MCzWLA=="
},
"@eslint/eslintrc": {
"version": "1.4.1",
@@ -11183,10 +11188,13 @@
"dev": true
},
"@types/node": {
- "version": "18.11.18",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
- "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
- "dev": true
+ "version": "20.12.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
+ "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~5.26.4"
+ }
},
"@types/node-fetch": {
"version": "2.6.2",
@@ -15647,6 +15655,12 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
diff --git a/package.json b/package.json
index 3092756..9bb0d23 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@tcgdex/sdk",
- "version": "2.5.1",
+ "version": "2.6.0-beta.1",
"main": "./dist/tcgdex.node.js",
"module": "./dist/tcgdex.node.mjs",
"types": "./dist/tcgdex.node.d.ts",
@@ -53,8 +53,8 @@
"node": ">=12"
},
"dependencies": {
- "isomorphic-unfetch": "^3",
- "unfetch": "^4"
+ "@dzeio/object-util": "^1",
+ "isomorphic-unfetch": "^3"
},
"scripts": {
"prebuild": "node scripts/export-version-number.js",
diff --git a/src/Psr/SimpleCache/CacheAbstract.ts b/src/Psr/SimpleCache/CacheAbstract.ts
new file mode 100644
index 0000000..68c2b24
--- /dev/null
+++ b/src/Psr/SimpleCache/CacheAbstract.ts
@@ -0,0 +1,38 @@
+import { objectLoop } from '@dzeio/object-util'
+import type CacheInterface from './CacheInterface'
+
+export default abstract class CacheAsbract implements CacheInterface {
+
+ public getMultiple(keys: Array, defaultValues?: Array | undefined): Record {
+ const res: Record = {}
+ 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(values: Record, ttl?: number | undefined): boolean {
+ objectLoop(values, (v, k) => {
+ this.set(k, v, ttl)
+ })
+ return true
+ }
+
+ public deleteMultiple(keys: Array): boolean {
+ for (const key of keys) {
+ this.delete(key)
+ }
+ return true
+ }
+
+ public abstract get(key: string, defaultValue?: T): T | undefined
+ public abstract set(key: string, value: T, ttl?: number): boolean
+ public abstract delete(key: string): boolean
+ public abstract clear(): boolean
+ public abstract has(key: string): boolean
+}
diff --git a/src/Psr/SimpleCache/CacheInterface.d.ts b/src/Psr/SimpleCache/CacheInterface.d.ts
new file mode 100644
index 0000000..b1556e9
--- /dev/null
+++ b/src/Psr/SimpleCache/CacheInterface.d.ts
@@ -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(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(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(keys: Array, defaultValues?: Array): Record
+
+ /**
+ * 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(values: Record, 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): 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
+}
diff --git a/src/Psr/SimpleCache/LocalStorageCache.ts b/src/Psr/SimpleCache/LocalStorageCache.ts
new file mode 100644
index 0000000..dc25b1b
--- /dev/null
+++ b/src/Psr/SimpleCache/LocalStorageCache.ts
@@ -0,0 +1,91 @@
+import CacheAsbract from './CacheAbstract'
+
+interface CacheItem {
+ 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(key: string, defaultValue?: T | undefined): T | undefined {
+ const raw = this.storage.getItem(this.getFinalKey(key))
+
+ if (!raw) {
+ return defaultValue ?? undefined
+ }
+
+ const item: CacheItem = JSON.parse(raw)
+
+ if (item.expire && item.expire < new Date().getTime()) {
+ this.delete(key)
+ return defaultValue ?? undefined
+ }
+
+ return item.data
+ }
+
+ public set(key: string, value: T, ttl?: number | undefined): boolean {
+ let expire = undefined
+ if (ttl) {
+ expire = (new Date()).getTime() + (ttl * 1000)
+ }
+ const data: CacheItem = {
+ 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 {
+ const list: Array = []
+ 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}`
+ }
+}
diff --git a/src/Psr/SimpleCache/MemoryCache.ts b/src/Psr/SimpleCache/MemoryCache.ts
new file mode 100644
index 0000000..15458b9
--- /dev/null
+++ b/src/Psr/SimpleCache/MemoryCache.ts
@@ -0,0 +1,58 @@
+import CacheAsbract from './CacheAbstract'
+
+interface CacheItem {
+ 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> = new Map()
+
+ public get(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(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)
+ }
+}
diff --git a/src/Query.ts b/src/Query.ts
new file mode 100644
index 0000000..682822c
--- /dev/null
+++ b/src/Query.ts
@@ -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
+ }
+}
diff --git a/src/Request.ts b/src/Request.ts
deleted file mode 100644
index 7ca38bd..0000000
--- a/src/Request.ts
+++ /dev/null
@@ -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 = {}
-
- public static async fetch(url: string): Promise {
- 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
- }
-
-}
diff --git a/src/endpoints/Endpoint.ts b/src/endpoints/Endpoint.ts
new file mode 100644
index 0000000..8177d87
--- /dev/null
+++ b/src/endpoints/Endpoint.ts
@@ -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- {
+ 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
- {
+ 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> {
+ const res = await this.tcgdex.fetchWithQuery([this.endpoint], query?.params)
+ return (res as Array