1
0
mirror of https://github.com/tcgdex/cards-database.git synced 2025-06-12 15:59:18 +00:00

feature: Implement new Server infrastructure with GraphQL. (#132)

* Added new compiler to db

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Add compiled DB to artifacts

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Fixed space error

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Fixed?

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Update node.js.yml

* Update node.js.yml

* Made change so the db is no longer dependent on the SDK

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* f

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Fixed artifact

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* U

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* \Changed folder

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Fixede?

Signed-off-by: Avior <florian.bouillon@delta-wings.net>

* Try with everything

* saved the file ;)

* ignore compiler

* Fixed prebuild being run again

* Fixed public folder

Signed-off-by: Avior <github@avior.me>

* fixed graphql file

Signed-off-by: Avior <github@avior.me>

* fixed?

Signed-off-by: Avior <github@avior.me>

* Check tree because life is potato

Signed-off-by: Avior <github@avior.me>

* this is harder

Signed-off-by: Avior <github@avior.me>

* f

Signed-off-by: Avior <github@avior.me>

* Fixed?

Signed-off-by: Avior <github@avior.me>

* r

Signed-off-by: Avior <github@avior.me>

* fd

Signed-off-by: Avior <github@avior.me>

* added back context

Signed-off-by: Avior <github@avior.me>

* ah

Signed-off-by: Avior <github@avior.me>

* AAH

Signed-off-by: Avior <github@avior.me>

* AAAH

Signed-off-by: Avior <github@avior.me>

* ffffffffffffffffff

Signed-off-by: Avior <github@avior.me>

* fix: Changed the default builder

Signed-off-by: Avior <github@avior.me>

* Removed useless tree function

Signed-off-by: Avior <github@avior.me>
This commit is contained in:
2021-11-04 10:02:26 +01:00
committed by GitHub
parent c991193f4b
commit c4b4449fd4
37 changed files with 5038 additions and 70 deletions

6
server/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/dist
/generated
/public/**/graphql.gql
/public/**/api.d.ts
/public/**/openapi.yaml

5
server/README.md Normal file
View File

@ -0,0 +1,5 @@
# TCGdex/server
The server running the TCGdex API.
More informations at https://github.com/tcgdex/cards-database#pokemon-tcgdex

View File

@ -0,0 +1,7 @@
// eslint-disable-next-line @typescript-eslint/ban-types
export interface Endpoint<Index extends {} = {}, Item extends {} = {}, SubItem extends {} = {}, C = undefined> {
index(common: C): Promise<Index | undefined>
item(common: C): Promise<Record<string, Item> | undefined>
sub?(common: C, item: string): Promise<Record<string, SubItem> | undefined>
common?(): Promise<C>
}

View File

@ -0,0 +1,30 @@
import { Card as CardSingle, CardResume } from '../../../meta/definitions/api'
import { Card, Languages } from '../../../interfaces'
import { Endpoint } from '../compilerInterfaces'
import { cardToCardSimple, cardToCardSingle, getCards } from '../utils/cardUtil'
type CardList = Array<CardResume>
export default class implements Endpoint<CardList, CardSingle, Record<string, unknown>, Array<[string, Card]>> {
public constructor(
private lang: keyof Languages
) {}
public async index(common: Array<[string, Card]>): Promise<CardList> {
return Promise.all(common.map((c) => cardToCardSimple(c[0], c[1], this.lang)))
}
public async item(common: Array<[string, Card]>): Promise<Record<string, CardSingle>> {
const items: Record<string, CardSingle> = {}
for await (const card of common) {
items[`${card[1].set.id}-${card[0]}`] = await cardToCardSingle(card[0], card[1], this.lang)
}
return items
}
public async common(): Promise<Array<[string, Card]>> {
return getCards(this.lang)
}
}

View File

@ -0,0 +1,33 @@
import { Serie as SerieSingle, SerieResume } from '../../../meta/definitions/api'
import { Languages, Serie } from '../../../interfaces'
import { Endpoint } from '../compilerInterfaces'
import { getSeries, serieToSerieSimple, serieToSerieSingle } from '../utils/serieUtil'
type SerieList = Array<SerieResume>
export default class implements Endpoint<SerieList, SerieSingle, Record<string, any>, Array<Serie>> {
public constructor(
private lang: keyof Languages
) {}
public async index(common: Array<Serie>): Promise<Array<SerieResume>> {
return Promise.all(common.map((s) => serieToSerieSimple(s, this.lang)))
}
public async item(common: Array<Serie>): Promise<Record<string, SerieSingle>> {
const items: Record<string, SerieSingle> = {}
for await (const val of common) {
const gen = await serieToSerieSingle(val, this.lang)
const name = val.name[this.lang] as string
items[name] = gen
items[val.id] = gen
}
return items
}
public async common(): Promise<Array<Serie>> {
return getSeries(this.lang)
}
}

View File

@ -0,0 +1,57 @@
import { Set as SetSingle, Card as CardSingle, SetResume } from '../../../meta/definitions/api'
import { getSets, isSetAvailable, setToSetSimple, setToSetSingle } from '../utils/setUtil'
import { Languages, Set } from '../../../interfaces'
import { Endpoint } from '../compilerInterfaces'
import { cardToCardSingle, getCards } from '../utils/cardUtil'
type SetList = Array<SetResume>
export default class implements Endpoint<SetList, SetSingle, CardSingle, Array<Set>> {
public constructor(
private lang: keyof Languages
) {}
public async index(common: Array<Set>): Promise<SetList> {
const sets = common
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
const tmp: SetList = await Promise.all(sets.map((el) => setToSetSimple(el, this.lang)))
return tmp
}
public async item(common: Array<Set>): Promise<Record<string, SetSingle>> {
const sets = await Promise.all(common
.map((set) => setToSetSingle(set, this.lang)))
const res: Record<string, SetSingle> = {}
for (const set of sets) {
res[set.name] = set
res[set.id] = set
}
return res
}
public async common(): Promise<Array<Set>> {
return getSets(undefined, this.lang)
}
public async sub(common: Array<Set>, item: string): Promise<Record<string, CardSingle> | undefined> {
const set = common.find((s) => s.name[this.lang] === item || s.id === item)
if (!set || !isSetAvailable(set, this.lang)) {
return undefined
}
const lit = await getCards(this.lang, set)
const list: Record<string, CardSingle> = {}
for await (const card of lit) {
list[card[0]] = await cardToCardSingle(card[0], card[1], this.lang)
}
return list
}
}

63
server/compiler/index.ts Normal file
View File

@ -0,0 +1,63 @@
/* eslint-disable max-statements */
import { Endpoint } from './compilerInterfaces'
import { promises as fs } from 'fs'
import { fetchRemoteFile } from './utils/util'
const LANGS = ['en', 'fr', 'es', 'it', 'pt', 'de']
const DIST_FOLDER = './generated'
;(async () => {
const paths = (await fs.readdir('./compiler/endpoints')).filter((p) => p.endsWith('.ts'))
console.log('Prefetching pictures')
await fetchRemoteFile('https://assets.tcgdex.net/datas.json')
// Delete dist folder to be sure to have a clean base
try {
await fs.rm(DIST_FOLDER, {recursive: true})
} catch {}
console.log('Let\'s GO !')
// Process each languages
for await (const lang of LANGS) {
console.log('Processing', lang)
// loop through """endpoints"""
for await (const file of paths) {
// final folder path
const folder = `${DIST_FOLDER}/${lang}`
// Make the folder
await fs.mkdir(folder, {recursive: true})
// Import the """Endpoint"""
const Ep = (await import(`./endpoints/${file}`)).default
const endpoint = new Ep(lang) as Endpoint
console.log(file, 'Running Common')
let common: any | null = null
if (endpoint.common) {
common = await endpoint.common()
}
console.log(file, 'Running Item')
const item = await endpoint.item(common)
// Write to file
await fs.writeFile(`${folder}/${file.replace('.ts', '')}.json`, JSON.stringify(item))
console.log(file, 'Finished Item')
}
}
// Finally copy definitions files to the public folder :D
for await (const file of await fs.readdir('../meta/definitions')) {
await fs.copyFile('../meta/definitions/' + file, './public/v2/' + file)
}
})()

View File

@ -0,0 +1,153 @@
/* eslint-disable sort-keys */
import { setToSetSimple } from './setUtil'
import { cardIsLegal, DB_PATH, fetchRemoteFile, smartGlob } from './util'
import { Set, SupportedLanguages, Card, Types } from '../../../interfaces'
import { Card as CardSingle, CardResume } from '../../../meta/definitions/api'
import translate from './translationUtil'
export async function getCardPictures(cardId: string, card: Card, lang: SupportedLanguages): Promise<string | undefined> {
try {
const file = await fetchRemoteFile('https://assets.tcgdex.net/datas.json')
const fileExists = Boolean(file[lang]?.[card.set.serie.id]?.[card.set.id]?.[cardId])
if (fileExists) {
return `https://assets.tcgdex.net/${lang}/${card.set.serie.id}/${card.set.id}/${cardId}`
}
} catch {
return undefined
}
return undefined
}
export async function cardToCardSimple(id: string, card: Card, lang: SupportedLanguages): Promise<CardResume> {
const cardName = card.name[lang]
if (!cardName) {
throw new Error(`Card (${card.set.id}-${id}) has no name in (${lang})`)
}
const img = await getCardPictures(id, card, lang)
return {
id: `${card.set.id}-${id}`,
image: img,
localId: id,
name: cardName
}
}
// eslint-disable-next-line max-lines-per-function
export async function cardToCardSingle(localId: string, card: Card, lang: SupportedLanguages): Promise<CardSingle> {
const image = await getCardPictures(localId, card, lang)
if (!card.name[lang]) {
throw new Error(`Card (${localId}) dont exist in (${lang})`)
}
return {
category: translate('category', card.category, lang) as any,
id: `${card.set.id}-${localId}`,
illustrator: card.illustrator,
image,
localId,
name: card.name[lang] as string,
rarity: translate('rarity', card.rarity, lang) as any,
set: await setToSetSimple(card.set, lang),
variants: {
firstEdition: typeof card.variants?.firstEdition === 'boolean' ? card.variants.firstEdition : false,
holo: typeof card.variants?.holo === 'boolean' ? card.variants.holo : true,
normal: typeof card.variants?.normal === 'boolean' ? card.variants.normal : true,
reverse: typeof card.variants?.reverse === 'boolean' ? card.variants.reverse : true
},
dexId: card.dexId,
hp: card.hp,
types: card.types?.map((t) => translate('types', t, lang)) as Array<Types>,
evolveFrom: card.evolveFrom && card.evolveFrom[lang],
weight: card.weight,
description: card.description ? card.description[lang] as string : undefined,
level: card.level,
stage: translate('stage', card.stage, lang) as any,
suffix: translate('suffix', card.suffix, lang) as any,
item: card.item ? {
name: card.item.name[lang] as string,
effect: card.item.effect[lang] as string
} : undefined,
abilities: card.abilities?.map((el) => ({
type: translate('abilityType', el.type, lang) as any,
name: el.name[lang] as string,
effect: el.effect[lang] as string
})),
attacks: card.attacks?.map((el) => ({
cost: el.cost?.map((t) => translate('types', t, lang)) as Array<Types>,
name: el.name[lang] as string,
effect: el.effect ? el.effect[lang] : undefined,
damage: el.damage
})),
weaknesses: card.weaknesses?.map((el) => ({
type: translate('types', el.type, lang) as Types,
value: el.value
})),
resistances: card.resistances?.map((el) => ({
type: translate('types', el.type, lang) as Types,
value: el.value
})),
retreat: card.retreat,
effect: card.effect ? card.effect[lang] : undefined,
trainerType: translate('trainerType', card.trainerType, lang) as any,
energyType: translate('energyType', card.energyType, lang) as any,
regulationMark: card.regulationMark,
legal: {
standard: cardIsLegal('standard', card, localId),
expanded: cardIsLegal('expanded', card, localId)
}
}
}
/**
*
* @param setName the setname of the card
* @param id the local id of the card
* @returns [the local id, the Card object]
*/
export async function getCard(serie: string, setName: string, id: string): Promise<Card> {
return (await import(`../../${DB_PATH}/data/${serie}/${setName}/${id}.js`)).default
}
export async function getCards(lang: SupportedLanguages, set?: Set): Promise<Array<[string, Card]>> {
const cards = await smartGlob(`${DB_PATH}/data/${(set && set.serie.name.en) ?? '*'}/${(set && set.name.en) ?? '*'}/*.js`)
const list: Array<[string, Card]> = []
for (const path of cards) {
const id = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.'))
const setName = (set && set.name.en) ?? (() => {
const part1 = path.substr(0, path.lastIndexOf(id) - 1)
return part1.substr(part1.lastIndexOf('/') + 1)
})()
const serieName = (set && set.serie.name.en) ?? (() => {
const part1 = path.substr(0, path.lastIndexOf(setName) - 1)
return part1.substr(part1.lastIndexOf('/') + 1)
})()
// console.log(path, id, setName)
const c = await getCard(serieName, setName, id)
if (!c.name[lang]) {
continue
}
list.push([id, c])
}
// Sort by id when possible
return list.sort(([a], [b]) => {
const ra = parseInt(a, 10)
const rb = parseInt(b, 10)
if (!isNaN(ra) && !isNaN(rb)) {
return ra - rb
}
return a >= b ? 1 : -1
})
}

View File

@ -0,0 +1,66 @@
import { DB_PATH, smartGlob } from './util'
import { setToSetSimple, getSets } from './setUtil'
import { Serie, SupportedLanguages, Set } from '../../../interfaces'
import { Serie as SerieSingle, SerieResume } from '../../../meta/definitions/api'
export async function getSerie(name: string): Promise<Serie> {
return (await import(`../../${DB_PATH}/data/${name}.js`)).default
}
export async function isSerieAvailable(serie: Serie, lang: SupportedLanguages): Promise<boolean> {
if (!serie.name[lang]) {
return false
}
const sets = await getSets(serie.name.en, lang)
return sets.length > 0
}
export async function getSeries(lang: SupportedLanguages): Promise<Array<Serie>> {
let series: Array<Serie> = (await Promise.all((await smartGlob(`${DB_PATH}/data/*.js`))
// Find Serie's name
.map((it) => it.substring(it.lastIndexOf('/') + 1, it.length - 3))
// Fetch the Serie
.map((it) => getSerie(it))))
// Filter the serie if no name's exists in the selected lang
.filter((serie) => Boolean(serie.name[lang]))
// Filter available series
const isAvailable = await Promise.all(series.map((serie) => isSerieAvailable(serie, lang)))
series = series.filter((_, index) => isAvailable[index])
// Sort series by the first set release date
const tmp: Array<[Serie, Set | undefined]> = await Promise.all(series.map(async (it) => [
it,
(await getSets(it.name.en, lang))
.reduce<Set | undefined>((p, c) => p ? p.releaseDate < c.releaseDate ? p : c : c, undefined) as Set
] as [Serie, Set]))
return tmp.sort((a, b) => (a[1] ? a[1].releaseDate : '0') > (b[1] ? b[1].releaseDate : '0') ? 1 : -1).map((it) => it[0])
}
export async function serieToSerieSimple(serie: Serie, lang: SupportedLanguages): Promise<SerieResume> {
const setsTmp = await getSets(serie.name.en, lang)
const sets = await Promise.all(setsTmp
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
.map((el) => setToSetSimple(el, lang)))
const logo = sets.find((set) => set.logo)?.logo
return {
id: serie.id,
logo,
name: serie.name[lang] as string
}
}
export async function serieToSerieSingle(serie: Serie, lang: SupportedLanguages): Promise<SerieSingle> {
const setsTmp = await getSets(serie.name.en, lang)
const sets = await Promise.all(setsTmp
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
.map((el) => setToSetSimple(el, lang)))
const logo = sets.find((set) => set.logo)?.logo
return {
id: serie.id,
logo,
name: serie.name[lang] as string,
sets
}
}

View File

@ -0,0 +1,104 @@
import { Set, SupportedLanguages } from '../../../interfaces'
import { DB_PATH, fetchRemoteFile, setIsLegal, smartGlob } from './util'
import { cardToCardSimple, getCards } from './cardUtil'
import { SetResume, Set as SetSingle } from '../../../meta/definitions/api'
interface t {
[key: string]: Set
}
const setCache: t = {}
export function isSetAvailable(set: Set, lang: SupportedLanguages): boolean {
return lang in set.name && lang in set.serie.name
}
/**
* Return the set
* @param name the name of the set (don't include.js/.ts)
*/
export async function getSet(name: string, serie = '*'): Promise<Set> {
if (!setCache[name]) {
try {
const [path] = await smartGlob(`${DB_PATH}/data/${serie}/${name}.js`)
setCache[name] = (await import('../../' + path)).default
} catch (error) {
console.error(error)
console.error(`Error trying to import importing (${`db/data/${serie}/${name}.js`})`)
process.exit(1)
}
}
return setCache[name]
}
// Dont use cache as it wont necessary have them all
export async function getSets(serie = '*', lang: SupportedLanguages): Promise<Array<Set>> {
// list sets names
const rawSets = (await smartGlob(`${DB_PATH}/data/${serie}/*.js`)).map((set) => set.substring(set.lastIndexOf('/') + 1, set.lastIndexOf('.')))
// Fetch sets
const sets = (await Promise.all(rawSets.map((set) => getSet(set, serie))))
// Filter sets
.filter((set) => isSetAvailable(set, lang))
// Sort sets by release date
.sort((a, b) => a.releaseDate > b.releaseDate ? 1 : -1)
return sets
}
export async function getSetPictures(set: Set, lang: SupportedLanguages): Promise<[string | undefined, string | undefined]> {
try {
const file = await fetchRemoteFile('https://assets.tcgdex.net/datas.json')
const logoExists = file[lang]?.[set.serie.id]?.[set.id]?.logo ? `https://assets.tcgdex.net/${lang}/${set.serie.id}/${set.id}/logo` : undefined
const symbolExists = file.univ?.[set.serie.id]?.[set.id]?.symbol ? `https://assets.tcgdex.net/univ/${set.serie.id}/${set.id}/symbol` : undefined
return [
logoExists,
symbolExists
]
} catch {
return [undefined, undefined]
}
}
export async function setToSetSimple(set: Set, lang: SupportedLanguages): Promise<SetResume> {
const cards = await getCards(lang, set)
const pics = await getSetPictures(set, lang)
return {
cardCount: {
official: set.cardCount.official,
total: Math.max(set.cardCount.official, cards.length)
},
id: set.id,
logo: pics[0],
name: set.name[lang] as string,
symbol: pics[1]
}
}
export async function setToSetSingle(set: Set, lang: SupportedLanguages): Promise<SetSingle> {
const cards = await getCards(lang, set)
const pics = await getSetPictures(set, lang)
return {
cardCount: {
firstEd: cards.reduce((count, card) => count + (card[1].variants?.firstEdition ? 1 : 0), 0),
holo: cards.reduce((count, card) => count + (card[1].variants?.holo ? 1 : 0), 0),
normal: cards.reduce((count, card) => count + (card[1].variants?.normal ? 1 : 0), 0),
official: set.cardCount.official,
reverse: cards.reduce((count, card) => count + (card[1].variants?.reverse ? 1 : 0), 0),
total: Math.max(set.cardCount.official, cards.length)
},
cards: await Promise.all(cards.map(([id, card]) => cardToCardSimple(id, card, lang))),
id: set.id,
legal: {
expanded: setIsLegal('expanded', set),
standard: setIsLegal('standard', set)
},
logo: pics[0],
name: set.name[lang] as string,
releaseDate: set.releaseDate,
serie: {
id: set.serie.id,
name: set.serie.name[lang] as string
},
symbol: pics[1],
tcgOnline: set.tcgOnline
}
}

View File

@ -0,0 +1,31 @@
import { SupportedLanguages } from '../../../interfaces'
import es from '../../../meta/translations/es.json'
import it from '../../../meta/translations/it.json'
import pt from '../../../meta/translations/pt.json'
import de from '../../../meta/translations/de.json'
import fr from '../../../meta/translations/fr.json'
type translatable = 'types' | 'rarity' | 'stage' | 'category' | 'suffix' | 'abilityType' | 'trainerType' | 'energyType'
const translations: Record<string, Record<translatable, Record<string, string>>> = {
es,
fr,
it,
pt,
de
}
export default function translate(item: translatable, key: string | undefined, lang: SupportedLanguages): string | undefined {
if (!key) {
return key
}
// Temporary trenslations are in english while they are being worked on
if (lang === 'en' || !Object.keys(translations).includes(lang)) {
return key
}
const res = translations[lang]?.[item]?.[key]
if (!res) {
throw new Error(`Could not find translation for ${lang}.${item}.${key}`)
}
return res
}

View File

@ -0,0 +1,59 @@
import { Card, Set } from '../../../interfaces'
import glob from 'glob'
import fetch from 'node-fetch'
import * as legals from '../../../meta/legals'
interface fileCacheInterface {
[key: string]: any
}
export const DB_PATH = "../../"
const fileCache: fileCacheInterface = {}
export async function fetchRemoteFile<T = any>(url: string): Promise<T> {
if (!fileCache[url]) {
const resp = await fetch(url, {
timeout: 60 * 1000
})
fileCache[url] = resp.json()
}
return fileCache[url]
}
const globCache: Record<string, Array<string>> = {}
export async function smartGlob(query: string): Promise<Array<string>> {
if (!globCache[query]) {
globCache[query] = await new Promise((res) => {
glob(query, (_, matches) => res(matches))
})
}
return globCache[query]
}
export function cardIsLegal(type: 'standard' | 'expanded', card: Card, localId: string): boolean {
const legal = legals[type]
if (
legal.includes.series.includes(card.set.serie.id) ||
legal.includes.sets.includes(card.set.id) ||
card.regulationMark && legal.includes.regulationMark.includes(card.regulationMark)
) {
return !(
legal.excludes.sets.includes(card.set.id) ||
legal.excludes.cards.includes(`${card.set.id}-${localId}`)
)
}
return false
}
export function setIsLegal(type: 'standard' | 'expanded', set: Set): boolean {
const legal = legals[type]
if (
legal.includes.series.includes(set.serie.id) ||
legal.includes.sets.includes(set.id)
) {
return !legal.excludes.sets.includes(set.id)
}
return false
}

2840
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
server/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "@tcgdex/server",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"compile": "ts-node compiler/index.ts",
"dev": "ts-node-dev -T src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node dist/index.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"@dzeio/config": "^1.1.4",
"@dzeio/object-util": "^1.4.2",
"@tcgdex/sdk": "^2.4.5",
"apicache": "^1.6.3",
"express": "^4.17.1",
"express-graphql": "^0.12.0",
"graphql": "^15.7.0",
"js2xmlparser": "^4.0.1"
},
"devDependencies": {
"@types/apicache": "^1.6.0",
"@types/express": "^4.17.13",
"@types/glob": "^7.2.0",
"@types/node": "^16.11.6",
"@types/node-fetch": "^2.5.12",
"fs-extra": "^10.0.0",
"glob": "^7.2.0",
"node-fetch": "^2.6.6",
"ts-node": "^10.4.0",
"ts-node-dev": "^1.1.8",
"typescript": "^4.4.4"
}
}

BIN
server/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

View File

@ -0,0 +1,112 @@
import { objectLoop } from '@dzeio/object-util'
import { Card as SDKCard, CardResume, SupportedLanguages } from '@tcgdex/sdk'
import Set from './Set'
import { Pagination } from '../../interfaces'
type LocalCard = Omit<SDKCard, 'set'> & {set: () => Set}
interface variants {
normal?: boolean;
reverse?: boolean;
holo?: boolean;
firstEdition?: boolean;
}
export default class Card implements LocalCard {
illustrator?: string | undefined
rarity!: string
category!: string
variants?: variants | undefined
dexId?: number[] | undefined
hp?: number | undefined
types?: string[] | undefined
evolveFrom?: string | undefined
weight?: string | undefined
description?: string | undefined
level?: string | number | undefined
stage?: string | undefined
suffix?: string | undefined
item?: { name: string; effect: string } | undefined
abilities?: { type: string; name: string; effect: string }[] | undefined
attacks?: { cost?: string[] | undefined; name: string; effect?: string | undefined; damage?: string | number | undefined }[] | undefined
weaknesses?: { type: string; value?: string | undefined }[] | undefined
resistances?: { type: string; value?: string | undefined }[] | undefined
retreat?: number | undefined
effect?: string | undefined
trainerType?: string | undefined
energyType?: string | undefined
regulationMark?: string | undefined
legal!: { standard: boolean; expanded: boolean }
id!: string
localId!: string
name!: string
image?: string | undefined
public constructor(
private lang: SupportedLanguages,
private card: SDKCard
) {
objectLoop(card, (it, key) => {
if (key === 'set') {
return
}
this[key as 'id'] = it
})
}
public set(): Set {
return Set.findOne(this.lang, {id: this.card.set.id}) as Set
}
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKCard, any>> = {}, pagination?: Pagination) {
let list : Array<SDKCard> = (require(`../../../generated/${lang}/cards.json`) as Array<SDKCard>)
.filter((c) => objectLoop(params, (it, key) => {
if (typeof it === "string") {
return c[key as 'localId'].toLowerCase().includes(it.toLowerCase())
}
return c[key as 'localId'].includes(it)
}))
if (pagination) {
list = list
.splice(pagination.count * pagination.page - 1, pagination.count)
}
return list.map((it) => new Card(lang, it))
}
public static raw(lang: SupportedLanguages): Array<SDKCard> {
return require(`../../generated/${lang}/cards.json`)
}
public static findOne(lang: SupportedLanguages, params: Partial<Record<keyof SDKCard, any>> = {}) {
const res = (require(`../../generated/${lang}/cards.json`) as Array<SDKCard>).find((c) => {
return objectLoop(params, (it, key) => {
if (key === 'set') {
return c['set'].id.includes(it) || c['set'].name.includes(it)
}
if (typeof it === "string") {
return c[key as 'localId'].toLowerCase().includes(it.toLowerCase())
}
return c[key as 'localId'].includes(it)
})
})
if (!res) {
return undefined
}
return new Card(lang, res)
}
public resume(): CardResume {
return {
id: this.id,
localId: this.localId,
name: this.name,
image: this.image
}
}
public full(): SDKCard {
return this.card
}
}

View File

@ -0,0 +1,66 @@
import { objectLoop } from '@dzeio/object-util'
import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
import Set from './Set'
import { Pagination } from '../../interfaces'
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<Set>}
export default class Serie implements LocalSerie {
id!: string
name!: string
logo?: string | undefined
public constructor(
private lang: SupportedLanguages,
private serie: SDKSerie
) {
objectLoop(serie, (it, key) => {
if (key === 'sets') {
return
}
this[key as 'id'] = it
})
}
public sets(): Array<Set> {
return this.serie.sets.map((s) => Set.findOne(this.lang, {id: s.id}) as Set)
}
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKSerie, any>> = {}, pagination?: Pagination) {
let list = (require(`../../generated/${lang}/series.json`) as Array<SDKSerie>)
.filter((c) => objectLoop(params, (it, key) => {
return c[key as 'id'].includes(it)
}))
if (pagination) {
list = list
.splice(pagination.count * pagination.page - 1, pagination.count)
}
return list.map((it) => new Serie(lang, it))
}
public static findOne(lang: SupportedLanguages, params: Partial<Record<keyof Serie, any>> = {}): Serie | undefined {
const res = (require(`../../../generated/${lang}/series.json`) as Array<SDKSerie>)
.find((c) => {
return objectLoop(params, (it, key) => {
return c[key as 'id'].includes(it)
})
})
if (!res) {
return undefined
}
return new Serie(lang, res)
}
public resume(): SerieResume {
return {
id: this.id,
name: this.name,
logo: this.logo
}
}
public full(): SDKSerie {
return this.serie
}
}

View File

@ -0,0 +1,89 @@
import { objectLoop } from '@dzeio/object-util'
import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
import Card from './Card'
import { Pagination } from '../../interfaces'
import Serie from './Serie'
interface variants {
normal?: boolean;
reverse?: boolean;
holo?: boolean;
firstEdition?: boolean;
}
type LocalSet = {serie: () => Serie, cards: () => Array<Card>} & Omit<SDKSet, 'serie' | 'cards'>
export default class Set implements LocalSet {
public constructor(
private lang: SupportedLanguages,
private set: SDKSet
) {
objectLoop(set, (it, key) => {
if (key === 'serie' || key === 'cards') {
return
}
this[key as 'id'] = it
})
}
tcgOnline?: string | undefined
variants?: variants | undefined
releaseDate!: string
legal!: { standard: boolean; expanded: boolean }
cardCount!: { total: number; official: number; normal: number; reverse: number; holo: number; firstEd?: number | undefined }
id!: string
name!: string
logo?: string | undefined
symbol?: string | undefined
public serie(): Serie {
return Serie.findOne(this.lang, {id: this.set.serie.id}) as Serie
}
public cards(): Array<Card> {
return this.set.cards.map((s) => Card.findOne(this.lang, {id: s.id}) as Card)
}
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKSet, any>> = {}, pagination?: Pagination) {
let list = (require(`../../../generated/${lang}/sets.json`) as Array<SDKSet>)
.filter((c) => objectLoop(params, (it, key) => {
return c[key as 'id'].includes(it)
}))
if (pagination) {
list = list
.splice(pagination.count * pagination.page - 1, pagination.count)
}
return list.map((it) => new Set(lang, it))
}
public static findOne(lang: SupportedLanguages, params: Partial<Record<keyof Set, any>> = {}) {
const res = (require(`../../../generated/${lang}/sets.json`) as Array<SDKSet>).find((c) => {
return objectLoop(params, (it, key) => {
return c[key as 'id'].includes(it)
})
})
if (!res) {
return undefined
}
return new Set(lang, res)
}
public resume(): SetResume {
return {
id: this.id,
name: this.name,
logo: this.logo,
symbol: this.symbol,
cardCount: {
total: this.cardCount.total,
official: this.cardCount.official
}
}
}
public full(): SDKSet {
return this.set
}
}

View File

@ -0,0 +1,212 @@
import { objectKeys, objectSize } from '@dzeio/object-util'
import { Card as SDKCard } from '@tcgdex/sdk'
import Card from '../Components/Card'
import Serie from '../Components/Serie'
import Set from '../Components/Set'
import express from 'express'
import apicache from 'apicache'
import { betterSorter, checkLanguage, sendError, unique } from '../../util'
const server = express.Router()
const endpointToField: Record<string, keyof SDKCard> = {
"categories": 'category',
'energy-types': 'energyType',
"hp": 'hp',
'illustrators': 'illustrator',
"rarities": 'rarity',
'regulation-marks': 'regulationMark',
"retreats": 'retreat',
"stages": "stage",
"suffixes": "suffix",
"trainer-types": "trainerType",
// fields that need special care
'dex-ids': 'dexId',
"sets": "set",
"types": "types",
"variants": "variants",
}
// server
// .get('/cache/performance', (req, res) => {
// res.json(apicache.getPerformance())
// })
// // add route to display cache index
// .get('/cache/index', (req, res) => {
// res.json(apicache.getIndex())
// })
server
.use(apicache.middleware('1 day', undefined, {}))
/**
* Listing Endpoint
* ex: /v2/en/cards
*/
.get('/:lang/:endpoint', (req, res): void => {
let { lang, endpoint } = req.params
if (endpoint.endsWith('.json')) {
endpoint = endpoint.replace('.json', '')
}
if (!checkLanguage(lang)) {
return sendError('LanguageNotFoundError', res, lang)
}
let result: any
switch (endpoint) {
case 'cards':
// to be quicker we directly return the raw file
if (objectSize(req.query) === 0) {
result = Card.raw(lang)
return
} else {
result = Card
.find(lang, req.query)
.map((c) => c.resume())
}
break
case 'sets':
result = Set
.find(lang, req.query)
.map((c) => c.resume())
break
case 'series':
result = Serie
.find(lang, req.query)
.map((c) => c.resume())
break
case 'categories':
case "energy-types":
case "hp":
case "illustrators":
case "rarities":
case "regulation-marks":
case "retreats":
case "series":
case "stages":
case "suffixes":
case "trainer-types":
result = unique(
Card.raw(lang)
.map((c) => c[endpointToField[endpoint]] as string)
.filter((c) => c)
).sort(betterSorter)
break
case "types":
case "dex-ids":
result = unique(
Card.raw(lang)
.map((c) => c[endpointToField[endpoint]] as Array<string>)
.filter((c) => c)
.reduce((p, c) => [...p, ...c], [] as Array<string>)
).sort(betterSorter)
break
case "variants":
result = unique(
Card.raw(lang)
.map((c) => objectKeys(c.variants ?? {}) as Array<string>)
.filter((c) => c)
.reduce((p, c) => [...p, ...c], [] as Array<string>)
).sort()
break
default:
sendError('EndpointNotFoundError', res, endpoint)
return
}
if (!result) {
sendError('NotFoundError', res)
}
res.json(result)
})
/**
* Listing Endpoint
* ex: /v2/en/cards/base1-1
*/
.get('/:lang/:endpoint/:id', (req, res) => {
let { id, lang, endpoint } = req.params
if (id.endsWith('.json')) {
id = id.replace('.json', '')
}
id = id.toLowerCase()
if (!checkLanguage(lang)) {
return sendError('LanguageNotFoundError', res, lang)
}
let result: any | undefined
switch (endpoint) {
case 'cards':
result = Card.findOne(lang, {id})?.full()
if (!result) {
result = Card.findOne(lang, {name: id})?.full()
}
break
case 'sets':
result = Set.findOne(lang, {id})?.full()
if (!result) {
result = Set.findOne(lang, {name: id})?.full()
}
break
case 'series':
result = Serie.findOne(lang, {id})?.full()
if (!result) {
result = Serie.findOne(lang, {name: id})?.full()
}
break
default:
result = Card.find(lang, {[endpointToField[endpoint]]: id})
}
if (!result) {
return res.status(404).send({error: "Endpoint or id not found"})
}
return res.send(result)
})
/**
* sub id Endpoint (for the set endpoint only currently)
* ex: /v2/en/sets/base1/1
*/
.get('/:lang/:endpoint/:id/:subid', (req, res) => {
let { id, lang, endpoint, subid } = req.params
if (subid.endsWith('.json')) {
subid = subid.replace('.json', '')
}
id = id.toLowerCase()
subid = subid.toLowerCase()
if (!checkLanguage(lang)) {
return sendError('LanguageNotFoundError', res, lang)
}
let result: any | undefined
switch (endpoint) {
case 'sets':
result = Card
.findOne(lang, {localId: subid, set: id})?.full()
break
}
if (!result) {
return sendError('NotFoundError', res)
}
return res.send(result)
})
export default server

View File

@ -0,0 +1,22 @@
import express from 'express'
import { graphqlHTTP } from 'express-graphql'
import { buildSchema } from 'graphql'
import resolver from './resolver'
import fs from 'fs'
// Init Express Router
const router = express.Router()
/**
* Drawbacks
* Attack.damage is a string instead of possibly being a number or a string
*/
const schema = buildSchema(fs.readFileSync('./public/v2/graphql.gql').toString())
// Add graphql to the route
router.use(graphqlHTTP({
schema,
rootValue: resolver,
graphiql: true
}))
export default router

View File

@ -0,0 +1,49 @@
import Card from '../Components/Card'
import { Options } from '../../interfaces'
import Serie from '../Components/Serie'
import Set from '../Components/Set'
import { SupportedLanguages } from '@tcgdex/sdk'
import { checkLanguage } from '../../util'
const middleware = <Q extends Record<string, any> = Record<string, any>>(fn: (lang: SupportedLanguages, query: Q) => any) => (
data: Q,
_: any,
e: any
) => {
let lang = e?.fieldNodes?.[0]?.directives?.[0]?.arguments?.[0]?.value?.value
if (!lang) {
lang = 'en'
}
if (!checkLanguage(lang)) {
return undefined
}
return fn(lang, data)
}
export default {
// Cards Endpoints
cards: middleware<Options<keyof Card['card']>>((lang, query) => {
return Card.find(lang, query.filters ?? {}, query.pagination)
}),
card: middleware<{set?: string, id: string}>((lang, query) => {
const toSearch = query.set ? 'localId' : 'id'
return Card.findOne(lang, {[toSearch]: query.id})
}),
// Set Endpoints
set: middleware<{id: string}>((lang, query) => {
return Set.findOne(lang, {id: query.id}) ?? Set.findOne(lang, {name: query.id})
}),
sets: middleware<Options<keyof Set['set']>>((lang, query) => {
return Set.find(lang, query.filters ?? {}, query.pagination)
}),
// Serie Endpoints
serie: middleware<{id: string}>((lang, query) => {
return Serie.findOne(lang, {id: query.id}) ?? Serie.findOne(lang, {name: query.id})
}),
series: middleware<Options<keyof Serie['serie']>>((lang, query) => {
return Serie.find(lang, query.filters ?? {}, query.pagination)
}),
};

35
server/src/index.ts Normal file
View File

@ -0,0 +1,35 @@
import express from 'express'
import graphql from './V2/graphql'
import jsonEndpoints from './V2/endpoints/jsonEndpoints'
// Current API version
const VERSION = 2
// Init Express server
const server = express()
// Set global headers
server.use((_, res, next) => {
res
.setHeader('Access-Control-Allow-Origin', '*')
.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
.setHeader('Access-Control-Allow-Headers', 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range')
.setHeader('Access-Control-Expose-Headers', 'Content-Length,Content-Range')
next()
})
server.get('/', (_, res) => {
res.redirect('https://www.tcgdex.net/docs?ref=api.tcgdex.net')
})
server.use(express.static('./public'))
// Setup GraphQL
server.use(`/v${VERSION}/graphql`, graphql)
// Setup JSON endpoints
server.use(`/v${VERSION}`, jsonEndpoints)
// Start server
server.listen(3000)
console.log(`🚀 Server ready at localhost:3000`);

11
server/src/interfaces.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { SupportedLanguages } from '@tcgdex/sdk'
export interface Pagination {
page: number
count: number
}
export interface Options<T> {
pagination?: Pagination
filters?: Partial<Record<T, any>>
}

57
server/src/util.ts Normal file
View File

@ -0,0 +1,57 @@
import { SupportedLanguages } from '@tcgdex/sdk'
import { Response } from 'express'
import fs from 'fs'
export function checkLanguage(str: string): str is SupportedLanguages {
return ['en', 'fr', 'es', 'it', 'pt', 'de'].includes(str)
}
export function unique(arr: Array<string>): Array<string> {
return arr.reduce((p, c) => p.includes(c) ? p : [...p, c], [] as Array<string>)
}
export function sendError(error: 'UnknownError' | 'NotFoundError' | 'LanguageNotFoundError' | 'EndpointNotFoundError', res: Response, v?: any) {
let message = ''
let status = 404
switch (error) {
case 'LanguageNotFoundError':
message = `Language not found (${v})`
break
case 'EndpointNotFoundError':
message = `Endpoint not found (${v})`
break
case 'NotFoundError':
message = 'The resource you are searching does not exists'
break
case 'UnknownError':
default:
message = `an unknown error occured (${v})`
status = 500
break
}
res.status(status).json({
message
}).end()
}
export function betterSorter(a: string, b: string) {
const ra = parseInt(a, 10)
const rb = parseInt(b, 10)
if (!isNaN(ra) && !isNaN(rb)) {
return ra - rb
}
return a >= b ? 1 : -1
}
export function tree(path: string, padding = 0) {
const folder = fs.readdirSync(path)
for (const file of folder) {
const filePath = path + '/' + file
console.log(filePath.padStart(padding, '-'))
try {
fs.lstatSync(filePath).isDirectory()
tree(filePath)
} catch {}
}
}

7
server/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "./node_modules/@dzeio/config/tsconfig.base",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}