1
0
mirror of https://github.com/tcgdex/cards-database.git synced 2025-06-28 23:09:18 +00:00

feat: Add advanced filtering capabilities (#522)

This commit is contained in:
2024-09-26 00:11:48 +02:00
committed by GitHub
parent 5899083e5d
commit 4700618047
15 changed files with 887 additions and 186 deletions

View File

@ -1,48 +1,47 @@
import { objectLoop } from '@dzeio/object-util'
import { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk'
import { Query } from '../../interfaces'
import { handlePagination, handleSort, handleValidation } from '../../util'
import Set from './Set'
import type { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk'
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
import TCGSet from './Set'
import en from '../../../generated/en/cards.json'
import fr from '../../../generated/fr/cards.json'
import es from '../../../generated/es/cards.json'
import it from '../../../generated/it/cards.json'
import pt from '../../../generated/pt/cards.json'
import ptbr from '../../../generated/pt-br/cards.json'
import ptpt from '../../../generated/pt-pt/cards.json'
import de from '../../../generated/de/cards.json'
import nl from '../../../generated/nl/cards.json'
import pl from '../../../generated/pl/cards.json'
import ru from '../../../generated/ru/cards.json'
import en from '../../../generated/en/cards.json'
import es from '../../../generated/es/cards.json'
import fr from '../../../generated/fr/cards.json'
import id from '../../../generated/id/cards.json'
import it from '../../../generated/it/cards.json'
import ja from '../../../generated/ja/cards.json'
import ko from '../../../generated/ko/cards.json'
import zhtw from '../../../generated/zh-tw/cards.json'
import id from '../../../generated/id/cards.json'
import nl from '../../../generated/nl/cards.json'
import pl from '../../../generated/pl/cards.json'
import ptbr from '../../../generated/pt-br/cards.json'
import ptpt from '../../../generated/pt-pt/cards.json'
import pt from '../../../generated/pt/cards.json'
import ru from '../../../generated/ru/cards.json'
import th from '../../../generated/th/cards.json'
import zhcn from '../../../generated/zh-cn/cards.json'
import zhtw from '../../../generated/zh-tw/cards.json'
const cards = {
'en': en,
'fr': fr,
'es': es,
'it': it,
'pt': pt,
en: en,
fr: fr,
es: es,
it: it,
pt: pt,
'pt-br': ptbr,
'pt-pt': ptpt,
'de': de,
'nl': nl,
'pl': pl,
'ru': ru,
'ja': ja,
'ko': ko,
de: de,
nl: nl,
pl: pl,
ru: ru,
ja: ja,
ko: ko,
'zh-tw': zhtw,
'id': id,
'th': th,
id: id,
th: th,
'zh-cn': zhcn,
} as const
type LocalCard = Omit<SDKCard, 'set'> & {set: () => Set}
type LocalCard = Omit<SDKCard, 'set'> & {set: () => TCGSet}
interface variants {
normal?: boolean;
@ -93,8 +92,8 @@ export default class Card implements LocalCard {
})
}
public set(): Set {
return Set.findOne(this.lang, {filters: { id: this.card.set.id }}) as Set
public set(): TCGSet {
return TCGSet.findOne(this.lang, { id: this.card.set.id }) as TCGSet
}
public static getAll(lang: SupportedLanguages): Array<SDKCard> {
@ -102,16 +101,15 @@ export default class Card implements LocalCard {
}
public static find(lang: SupportedLanguages, query: Query<SDKCard>) {
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
.map((it) => new Card(lang, it))
return executeQuery(Card.getAll(lang), query).data.map((it) => new Card(lang, it))
}
public static findOne(lang: SupportedLanguages, query: Query<SDKCard>) {
const res = handleSort(handleValidation(this.getAll(lang), query), query)
const res = Card.find(lang, query)
if (res.length === 0) {
return undefined
}
return new Card(lang, res[0])
return res[0]
}
public resume(): CardResume {

View File

@ -1,49 +1,48 @@
import { objectLoop } from '@dzeio/object-util'
import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
import { Query } from '../../interfaces'
import { handlePagination, handleSort, handleValidation } from '../../util'
import Set from './Set'
import type { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
import TCGSet from './Set'
import en from '../../../generated/en/series.json'
import fr from '../../../generated/fr/series.json'
import es from '../../../generated/es/series.json'
import it from '../../../generated/it/series.json'
import pt from '../../../generated/pt/series.json'
import ptbr from '../../../generated/pt-br/series.json'
import ptpt from '../../../generated/pt-pt/series.json'
import de from '../../../generated/de/series.json'
import nl from '../../../generated/nl/series.json'
import pl from '../../../generated/pl/series.json'
import ru from '../../../generated/ru/series.json'
import en from '../../../generated/en/series.json'
import es from '../../../generated/es/series.json'
import fr from '../../../generated/fr/series.json'
import id from '../../../generated/id/series.json'
import it from '../../../generated/it/series.json'
import ja from '../../../generated/ja/series.json'
import ko from '../../../generated/ko/series.json'
import zhtw from '../../../generated/zh-tw/series.json'
import id from '../../../generated/id/series.json'
import nl from '../../../generated/nl/series.json'
import pl from '../../../generated/pl/series.json'
import ptbr from '../../../generated/pt-br/series.json'
import ptpt from '../../../generated/pt-pt/series.json'
import pt from '../../../generated/pt/series.json'
import ru from '../../../generated/ru/series.json'
import th from '../../../generated/th/series.json'
import zhcn from '../../../generated/zh-cn/series.json'
import zhtw from '../../../generated/zh-tw/series.json'
const series = {
'en': en,
'fr': fr,
'es': es,
'it': it,
'pt': pt,
en: en,
fr: fr,
es: es,
it: it,
pt: pt,
'pt-br': ptbr,
'pt-pt': ptpt,
'de': de,
'nl': nl,
'pl': pl,
'ru': ru,
'ja': ja,
'ko': ko,
de: de,
nl: nl,
pl: pl,
ru: ru,
ja: ja,
ko: ko,
'zh-tw': zhtw,
'id': id,
'th': th,
id: id,
th: th,
'zh-cn': zhcn,
} as const
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<Set>}
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<TCGSet>}
export default class Serie implements LocalSerie {
@ -63,8 +62,8 @@ export default class Serie implements LocalSerie {
})
}
public sets(): Array<Set> {
return this.serie.sets.map((s) => Set.findOne(this.lang, {filters: { id: s.id }}) as Set)
public sets(): Array<TCGSet> {
return this.serie.sets.map((s) => TCGSet.findOne(this.lang, { id: s.id }) as TCGSet)
}
public static getAll(lang: SupportedLanguages): Array<SDKSerie> {
@ -72,16 +71,15 @@ export default class Serie implements LocalSerie {
}
public static find(lang: SupportedLanguages, query: Query<SDKSerie>) {
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
.map((it) => new Serie(lang, it))
return executeQuery(Serie.getAll(lang), query).data.map((it) => new Serie(lang, it))
}
public static findOne(lang: SupportedLanguages, query: Query<SDKSerie>) {
const res = handleValidation(this.getAll(lang), query)
const res = Serie.find(lang, query)
if (res.length === 0) {
return undefined
}
return new Serie(lang, res[0])
return res[0]
}
public resume(): SerieResume {

View File

@ -1,45 +1,44 @@
import { objectLoop } from '@dzeio/object-util'
import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
import { Query } from '../../interfaces'
import { handlePagination, handleSort, handleValidation } from '../../util'
import type { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
import Card from './Card'
import Serie from './Serie'
import en from '../../../generated/en/sets.json'
import fr from '../../../generated/fr/sets.json'
import es from '../../../generated/es/sets.json'
import it from '../../../generated/it/sets.json'
import pt from '../../../generated/pt/sets.json'
import ptbr from '../../../generated/pt-br/sets.json'
import ptpt from '../../../generated/pt-pt/sets.json'
import de from '../../../generated/de/sets.json'
import nl from '../../../generated/nl/sets.json'
import pl from '../../../generated/pl/sets.json'
import ru from '../../../generated/ru/sets.json'
import en from '../../../generated/en/sets.json'
import es from '../../../generated/es/sets.json'
import fr from '../../../generated/fr/sets.json'
import id from '../../../generated/id/sets.json'
import it from '../../../generated/it/sets.json'
import ja from '../../../generated/ja/sets.json'
import ko from '../../../generated/ko/sets.json'
import zhtw from '../../../generated/zh-tw/sets.json'
import id from '../../../generated/id/sets.json'
import nl from '../../../generated/nl/sets.json'
import pl from '../../../generated/pl/sets.json'
import ptbr from '../../../generated/pt-br/sets.json'
import ptpt from '../../../generated/pt-pt/sets.json'
import pt from '../../../generated/pt/sets.json'
import ru from '../../../generated/ru/sets.json'
import th from '../../../generated/th/sets.json'
import zhcn from '../../../generated/zh-cn/sets.json'
import zhtw from '../../../generated/zh-tw/sets.json'
const sets = {
'en': en,
'fr': fr,
'es': es,
'it': it,
'pt': pt,
en: en,
fr: fr,
es: es,
it: it,
pt: pt,
'pt-br': ptbr,
'pt-pt': ptpt,
'de': de,
'nl': nl,
'pl': pl,
'ru': ru,
'ja': ja,
'ko': ko,
de: de,
nl: nl,
pl: pl,
ru: ru,
ja: ja,
ko: ko,
'zh-tw': zhtw,
'id': id,
'th': th,
id: id,
th: th,
'zh-cn': zhcn,
} as const
@ -77,11 +76,11 @@ export default class Set implements LocalSet {
symbol?: string | undefined
public serie(): Serie {
return Serie.findOne(this.lang, {filters: { id: this.set.serie.id }}) as 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, { filters: { id: s.id }}) as Card)
return this.set.cards.map((s) => Card.findOne(this.lang, { id: s.id }) as Card)
}
public static getAll(lang: SupportedLanguages): Array<SDKSet> {
@ -89,16 +88,15 @@ export default class Set implements LocalSet {
}
public static find(lang: SupportedLanguages, query: Query<SDKSet>) {
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
.map((it) => new Set(lang, it))
return executeQuery(Set.getAll(lang), query).data.map((it) => new Set(lang, it))
}
public static findOne(lang: SupportedLanguages, query: Query<SDKSet>) {
const res = handleValidation(this.getAll(lang), query)
const res = Set.find(lang, query)
if (res.length === 0) {
return undefined
}
return new Set(lang, res[0])
return res[0]
}
public resume(): SetResume {

View File

@ -1,41 +1,42 @@
import { objectKeys, objectLoop } from '@dzeio/object-util'
import { Card as SDKCard } from '@tcgdex/sdk'
import { objectKeys } from '@dzeio/object-util'
import type { Card as SDKCard } from '@tcgdex/sdk'
import apicache from 'apicache'
import express, { Request } from 'express'
import { Query } from '../../interfaces'
import express, { type Request } from 'express'
import { Errors, sendError } from '../../libs/Errors'
import type { Query } from '../../libs/QueryEngine/filter'
import { recordToQuery } from '../../libs/QueryEngine/parsers'
import { betterSorter, checkLanguage, unique } from '../../util'
import Card from '../Components/Card'
import Serie from '../Components/Serie'
import Set from '../Components/Set'
import TCGSet from '../Components/Set'
type CustomRequest = Request & {
/**
* disable caching
*/
DO_NOT_CACHE?: boolean
advQuery?: Query<any>
advQuery?: Query
}
const server = express.Router()
const endpointToField: Record<string, keyof SDKCard> = {
"categories": 'category',
categories: 'category',
'energy-types': 'energyType',
"hp": 'hp',
'illustrators': 'illustrator',
"rarities": 'rarity',
hp: 'hp',
illustrators: 'illustrator',
rarities: 'rarity',
'regulation-marks': 'regulationMark',
"retreats": 'retreat',
"stages": "stage",
"suffixes": "suffix",
retreats: 'retreat',
stages: "stage",
suffixes: "suffix",
"trainer-types": "trainerType",
// fields that need special care
'dex-ids': 'dexId',
"sets": "set",
"types": "types",
"variants": "variants",
sets: "set",
types: "types",
variants: "variants",
}
server
@ -66,27 +67,7 @@ server
return
}
const items: Query = {
filters: undefined,
sort: undefined,
pagination: undefined
}
objectLoop(req.query as Record<string, string | Array<string>>, (value: string | Array<string>, key: string) => {
if (!key.includes(':')) {
key = 'filters:' + key
}
const [cat, item] = key.split(':', 2) as ['filters', string]
if (!items[cat]) {
items[cat] = {}
}
const finalValue = Array.isArray(value) ? value.map((it) => isNaN(parseInt(it)) ? it : parseInt(it)) : isNaN(parseInt(value)) ? value : parseInt(value)
// @ts-expect-error normal behavior
items[cat][item] = finalValue
})
req.advQuery = items
req.advQuery = recordToQuery(req.query as Record<string, string | Array<string>>)
next()
})
@ -102,15 +83,16 @@ server
return
}
// biome-ignore lint/style/noNonNullAssertion: <explanation>
const query: Query = req.advQuery!
let data: Array<Card | Set | Serie> = []
let data: Array<Card | TCGSet | Serie> = []
switch (what.toLowerCase()) {
case 'card':
data = Card.find(lang, query)
break
case 'set':
data = Set.find(lang, query)
data = TCGSet.find(lang, query)
break
case 'serie':
data = Serie.find(lang, query)
@ -132,7 +114,7 @@ server
.get('/:lang/:endpoint', (req: CustomRequest, res): void => {
let { lang, endpoint } = req.params
const query: Query = req.advQuery!
const query: Query = req.advQuery ?? {}
if (endpoint.endsWith('.json')) {
endpoint = endpoint.replace('.json', '')
@ -143,7 +125,7 @@ server
return
}
let result: any
let result: unknown
switch (endpoint) {
case 'cards':
@ -153,7 +135,7 @@ server
break
case 'sets':
result = Set
result = TCGSet
.find(lang, query)
.map((c) => c.resume())
break
@ -169,7 +151,6 @@ server
case "rarities":
case "regulation-marks":
case "retreats":
case "series":
case "stages":
case "suffixes":
case "trainer-types":
@ -224,26 +205,26 @@ server
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
}
let result: any | undefined
let result: unknown
switch (endpoint) {
case 'cards':
result = Card.findOne(lang, { filters: { id }, strict: true })?.full()
result = Card.findOne(lang, { id })?.full()
if (!result) {
result = Card.findOne(lang, { filters: { name: id }, strict: true })?.full()
result = Card.findOne(lang, { name: id })?.full()
}
break
case 'sets':
result = Set.findOne(lang, { filters: { id }, strict: true })?.full()
result = TCGSet.findOne(lang, { id })?.full()
if (!result) {
result = Set.findOne(lang, {filters: { name: id }, strict: true })?.full()
result = TCGSet.findOne(lang, { name: id })?.full()
}
break
case 'series':
result = Serie.findOne(lang, { filters: { id }, strict: true })?.full()
result = Serie.findOne(lang, { id })?.full()
if (!result) {
result = Serie.findOne(lang, { filters: { name: id }, strict: true })?.full()
result = Serie.findOne(lang, { name: id })?.full()
}
break
default:
@ -282,12 +263,14 @@ server
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
}
let result: any | undefined
let result: unknown
switch (endpoint) {
case 'sets':
// allow the dev to use a non prefixed value like `10` instead of `010` for newer sets
result = Card
.findOne(lang, { filters: { localId: subid, set: id }, strict: true})?.full()
// @ts-expect-error normal behavior until the filtering is more fiable
.findOne(lang, { localId: { $or: [subid.padStart(3, '0'), subid]}, 'set.id': id })?.full()
break
}
if (!result) {

View File

@ -1,6 +1,6 @@
import express from 'express'
import fs from 'fs'
import { buildSchema, GraphQLError } from 'graphql'
import fs from 'node:fs'
import { buildSchema, type GraphQLError } from 'graphql'
import { createHandler } from 'graphql-http/lib/use/express'
import { type ruruHTML as RuruHTML } from 'ruru/dist/server'
/** @ts-expect-error typing is not correctly mapped (real type at ruru/dist/server.d.ts) */

View File

@ -1,44 +1,54 @@
import { SupportedLanguages } from '@tcgdex/sdk'
import { Query } from '../../interfaces'
import type { SupportedLanguages } from '@tcgdex/sdk'
import { type Query, Sort } from '../../libs/QueryEngine/filter'
import { recordToQuery } from '../../libs/QueryEngine/parsers'
import { checkLanguage } from '../../util'
import Card from '../Components/Card'
import Serie from '../Components/Serie'
import Set from '../Components/Set'
const middleware = (fn: (lang: SupportedLanguages, query: Query) => any) => (
data: Query,
// TODO: make a better way to find the language
function getLang(e: any): SupportedLanguages {
// get the locale directive
const langArgument = e?.fieldNodes?.[0]?.directives?.[0]?.arguments?.[0]?.value
if (!langArgument) {
return 'en'
}
if (langArgument.kind === 'Variable') {
return e.variableValues[langArgument.name.value]
}
return langArgument.value
}
const middleware = (fn: (lang: SupportedLanguages, query: Query<object>) => any) => (
data: Record<string, any>,
_: any,
e: any
) => {
// get the locale directive
const langArgument = e?.fieldNodes?.[0]?.directives?.[0]?.arguments?.[0]?.value
const lang = getLang(e)
const query = recordToQuery(data.filters ?? {})
// Deprecated code handling
// @ts-expect-error count is deprectaed in the frontend
if (data.pagination?.count) {
// @ts-expect-error count is deprectaed in the frontend
data.pagination.itemsPerPage = data.pagination.count
if (data.pagination) {
query.$page = data.pagination.page ?? 1
query.$limit = data.pagination.itemsPerPage ?? 100
}
// if there is no locale directive
if (!langArgument) {
return fn('en', data)
}
// set default locale directive value
let lang = 'en'
// handle variable for directive value
if (langArgument.kind === 'Variable') {
lang = e.variableValues[langArgument.name.value]
} else {
lang = langArgument.value
if (data.sort) {
query.$sort = {
[data.sort.field]: data.sort.order === 'DESC' ? Sort.DESC : Sort.ASC
}
}
if (!checkLanguage(lang)) {
return undefined
}
return fn(lang, data)
return fn(lang, query)
}
export default {