1
0
mirror of https://github.com/tcgdex/cards-database.git synced 2025-04-22 10:52:10 +00:00

feat: Add better sorting/filtering/pagination (#458)

This commit is contained in:
Florian Bouillon 2024-01-03 02:27:56 +01:00 committed by GitHub
parent 034b7e2cec
commit 5c8ca20a41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 399 additions and 178 deletions

View File

@ -25,5 +25,8 @@ jobs:
cd server cd server
bun install --frozen-lockfile bun install --frozen-lockfile
- name: Validate the data - name: Validate the data & the server
run: bun run validate run: |
bun run validate
cd server
bun run validate

View File

@ -7,28 +7,62 @@ directive @locale (
lang: String! lang: String!
) on FIELD ) on FIELD
# Queries to use on the DB """
Every queries available on the GraphQL API
If you have more queries that you would like added, make a new issue here
https://github.com/tcgdex/cards-database/issues/new/choose
"""
type Query { type Query {
cards(filters: CardsFilters, pagination: Pagination): [Card] """Find the cards"""
sets: [Set] cards(filters: CardsFilters, pagination: Pagination, sort: Sort): [Card]
series: [Serie]
"""Find the sets"""
sets(filters: SetFilters, pagination: Pagination, sort: Sort): [Set]
"""Find the series"""
series(filters: SerieFilters, pagination: Pagination, sort: Sort): [Serie]
"""Find one card (using the id and set is deprecated)"""
card( card(
id: ID!, id: ID!,
set: String set: String,
"""The new way to filter"""
filters: CardsFilters
): Card ): Card
"""Find one set (using the id is deprecated)"""
set( set(
id: ID! id: ID!,
"""The new way to filter"""
filters: SetFilters
): Set ): Set
"""Find one serie (using the id is deprecated)"""
serie( serie(
id: ID! id: ID!,
"""The new way to filter"""
filters: SerieFilters
): Serie ): Serie
} }
# Pagination input """Paginate the datas you fetch"""
input Pagination { input Pagination {
page: Float! """Indicate the page number (from 1)"""
count: Float! page: Int!
"""Indicate the number of items in one page"""
itemsPerPage: Int
count: Float @deprecated(reason: "use itemsPerPage instead")
}
"""Change how the data is sorted"""
input Sort {
"""Indicate which field it will sort using"""
field: String!
"""Indicate how it is sorted ("ASC" or "DESC) (default: "ASC")"""
order: String
} }
################## ##################
@ -41,13 +75,13 @@ input CardsFilters {
description: String description: String
energyType: String energyType: String
evolveFrom: String evolveFrom: String
hp: Float hp: Int
id: ID id: ID
localId: String localId: String
dexId: Float dexId: Int
illustrator: String illustrator: String
image: String image: String
level: Float level: Int
levelId: String levelId: String
name: String name: String
rarity: String rarity: String
@ -55,7 +89,7 @@ input CardsFilters {
stage: String stage: String
suffix: String suffix: String
trainerType: String trainerType: String
retreat: Float retreat: Int
} }
type Card { type Card {
@ -63,23 +97,23 @@ type Card {
attacks: [AttacksListItem] attacks: [AttacksListItem]
category: String! category: String!
description: String description: String
dexId: [Float] dexId: [Int]
effect: String effect: String
energyType: String energyType: String
evolveFrom: String evolveFrom: String
hp: Float hp: Int
id: String! id: String!
illustrator: String illustrator: String
image: String image: String
item: Item item: Item
legal: Legal! legal: Legal!
level: Float level: Int
localId: String! localId: String!
name: String! name: String!
rarity: String! rarity: String!
regulationMark: String regulationMark: String
resistances: [WeakResListItem] resistances: [WeakResListItem]
retreat: Float retreat: Int
set: Set! set: Set!
stage: String stage: String
suffix: String suffix: String
@ -143,13 +177,21 @@ type Set {
tcgOnline: String tcgOnline: String
} }
input SetFilters {
id: String
name: String
serie: String
releaseDate: String
tcgOnline: String
}
type CardCount { type CardCount {
firstEd: Float firstEd: Int
holo: Float holo: Int
normal: Float normal: Int
official: Float! official: Int!
reverse: Float reverse: Int
total: Float! total: Int!
} }
################## ##################
@ -163,6 +205,11 @@ type Serie {
sets: [Set]! sets: [Set]!
} }
input SerieFilters {
id: String
name: String
}
################## ##################
# StringEndpoint # # StringEndpoint #
################## ##################

View File

@ -5,6 +5,7 @@
"scripts": { "scripts": {
"compile": "bun compiler/index.ts", "compile": "bun compiler/index.ts",
"dev": "bun --watch --hot src/index.ts", "dev": "bun --watch --hot src/index.ts",
"validate": "tsc --noEmit --project ./tsconfig.json",
"start": "bun src/index.ts" "start": "bun src/index.ts"
}, },
"license": "MIT", "license": "MIT",

View File

@ -1,7 +1,7 @@
import { objectLoop } from '@dzeio/object-util' import { objectLoop } from '@dzeio/object-util'
import { Card as SDKCard, CardResume, SupportedLanguages } from '@tcgdex/sdk' import { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk'
import { Pagination } from '../../interfaces' import { Query } from '../../interfaces'
import { lightCheck } from '../../util' import { handlePagination, handleSort, handleValidation } from '../../util'
import Set from './Set' import Set from './Set'
type LocalCard = Omit<SDKCard, 'set'> & {set: () => Set} type LocalCard = Omit<SDKCard, 'set'> & {set: () => Set}
@ -55,40 +55,25 @@ export default class Card implements LocalCard {
}) })
} }
public set(): Set { public set(): Set {
return Set.findOne(this.lang, {id: this.card.set.id}) as Set return Set.findOne(this.lang, {filters: { id: this.card.set.id }}) as Set
} }
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKCard, any>> = {}, pagination?: Pagination) { public static getAll(lang: SupportedLanguages): Array<SDKCard> {
let list : Array<SDKCard> = (require(`../../../generated/${lang}/cards.json`) as Array<SDKCard>)
.filter((c) => objectLoop(params, (it, key) => {
return lightCheck(c[key as 'localId'], 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`) return require(`../../../generated/${lang}/cards.json`)
} }
public static findOne(lang: SupportedLanguages, params: Partial<Record<keyof SDKCard, any>> = {}) { public static find(lang: SupportedLanguages, query: Query<SDKCard>) {
const res = (require(`../../../generated/${lang}/cards.json`) as Array<SDKCard>).find((c) => { return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
return objectLoop(params, (it, key) => { .map((it) => new Card(lang, it))
if (key === 'set' && typeof it === 'string') { }
return (c['set'].id === it || lightCheck(c['set'].name, it))
} public static findOne(lang: SupportedLanguages, query: Query<SDKCard>) {
return lightCheck(c[key as 'localId'], it) const res = handleValidation(this.getAll(lang), query)
}) if (res.length === 0) {
})
if (!res) {
return undefined return undefined
} }
return new Card(lang, res) return new Card(lang, res[0])
} }
public resume(): CardResume { public resume(): CardResume {

View File

@ -1,7 +1,7 @@
import { objectLoop } from '@dzeio/object-util' import { objectLoop } from '@dzeio/object-util'
import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk' import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
import { Pagination } from '../../interfaces' import { Query } from '../../interfaces'
import { lightCheck } from '../../util' import { handlePagination, handleSort, handleValidation } from '../../util'
import Set from './Set' import Set from './Set'
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<Set>} type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<Set>}
@ -25,34 +25,24 @@ export default class Serie implements LocalSerie {
} }
public sets(): Array<Set> { public sets(): Array<Set> {
return this.serie.sets.map((s) => Set.findOne(this.lang, {id: s.id}) as Set) return this.serie.sets.map((s) => Set.findOne(this.lang, {filters: { id: s.id }}) as Set)
} }
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKSerie, any>> = {}, pagination?: Pagination) { public static getAll(lang: SupportedLanguages): Array<SDKSerie> {
let list = (require(`../../../generated/${lang}/series.json`) as Array<SDKSerie>) return require(`../../../generated/${lang}/series.json`)
.filter((c) => objectLoop(params, (it, key) => {
if (key === 'id') return c[key] === it
return lightCheck(c[key as 'id'], 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 { public static find(lang: SupportedLanguages, query: Query<SDKSerie>) {
const res = (require(`../../../generated/${lang}/series.json`) as Array<SDKSerie>) return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
.find((c) => { .map((it) => new Serie(lang, it))
return objectLoop(params, (it, key) => { }
if (key === 'id') return c[key] === it
return lightCheck(c[key as 'id'], it) public static findOne(lang: SupportedLanguages, query: Query<SDKSerie>) {
}) const res = handleValidation(this.getAll(lang), query)
}) if (res.length === 0) {
if (!res) {
return undefined return undefined
} }
return new Serie(lang, res) return new Serie(lang, res[0])
} }
public resume(): SerieResume { public resume(): SerieResume {

View File

@ -1,7 +1,7 @@
import { objectLoop } from '@dzeio/object-util' import { objectLoop } from '@dzeio/object-util'
import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk' import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
import { Pagination } from '../../interfaces' import { Query } from '../../interfaces'
import { lightCheck } from '../../util' import { handlePagination, handleSort, handleValidation } from '../../util'
import Card from './Card' import Card from './Card'
import Serie from './Serie' import Serie from './Serie'
@ -39,45 +39,28 @@ export default class Set implements LocalSet {
symbol?: string | undefined symbol?: string | undefined
public serie(): Serie { public serie(): Serie {
return Serie.findOne(this.lang, {id: this.set.serie.id}) as Serie return Serie.findOne(this.lang, {filters: { id: this.set.serie.id }}) as Serie
} }
public cards(): Array<Card> { public cards(): Array<Card> {
return this.set.cards.map((s) => Card.findOne(this.lang, {id: s.id}) as Card) return this.set.cards.map((s) => Card.findOne(this.lang, { filters: { id: s.id }}) as Card)
} }
public static find(lang: SupportedLanguages, params: Partial<Record<keyof SDKSet, any>> = {}, pagination?: Pagination) { public static getAll(lang: SupportedLanguages): Array<SDKSet> {
let list = (require(`../../../generated/${lang}/sets.json`) as Array<SDKSet>) return require(`../../../generated/${lang}/sets.json`)
.filter((c) => objectLoop(params, (it, key) => {
if (key === 'id' || key === 'name') {
return c[key as 'id'].toLowerCase() === it.toLowerCase()
} else if (typeof it === 'string') {
return c[key as 'id'].toLowerCase().includes(it.toLowerCase())
}
return lightCheck(c[key as 'id'], 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>> = {}) { public static find(lang: SupportedLanguages, query: Query<SDKSet>) {
const res = (require(`../../../generated/${lang}/sets.json`) as Array<SDKSet>).find((c) => { return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
return objectLoop(params, (it, key) => { .map((it) => new Set(lang, it))
if (key === 'id' || key === 'name') { }
return c[key as 'id'].toLowerCase() === it.toLowerCase()
} else if (typeof it === 'string') { public static findOne(lang: SupportedLanguages, query: Query<SDKSet>) {
return c[key as 'id'].toLowerCase().includes(it.toLowerCase()) const res = handleValidation(this.getAll(lang), query)
} if (res.length === 0) {
return lightCheck(c[key as 'id'], it)
})
})
if (!res) {
return undefined return undefined
} }
return new Set(lang, res) return new Set(lang, res[0])
} }
public resume(): SetResume { public resume(): SetResume {

View File

@ -1,11 +1,12 @@
import { objectKeys } from '@dzeio/object-util' import { objectKeys, objectLoop } from '@dzeio/object-util'
import { Card as SDKCard } from '@tcgdex/sdk' import { Card as SDKCard } from '@tcgdex/sdk'
import apicache from 'apicache'
import express from 'express'
import { Query } from '../../interfaces'
import { betterSorter, checkLanguage, sendError, unique } from '../../util'
import Card from '../Components/Card' import Card from '../Components/Card'
import Serie from '../Components/Serie' import Serie from '../Components/Serie'
import Set from '../Components/Set' 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 server = express.Router()
@ -48,6 +49,40 @@ server
next() next()
}) })
// handle Query builder
.use((req, _, next) => {
// handle no query
if (!req.query) {
next()
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
})
console.log(items)
// @ts-expect-error normal behavior
req.advQuery = items
next()
})
/** /**
* Listing Endpoint * Listing Endpoint
@ -56,6 +91,9 @@ server
.get('/:lang/:endpoint', (req, res): void => { .get('/:lang/:endpoint', (req, res): void => {
let { lang, endpoint } = req.params let { lang, endpoint } = req.params
// @ts-expect-error normal behavior
const query: Query = req.advQuery
if (endpoint.endsWith('.json')) { if (endpoint.endsWith('.json')) {
endpoint = endpoint.replace('.json', '') endpoint = endpoint.replace('.json', '')
} }
@ -69,18 +107,18 @@ server
switch (endpoint) { switch (endpoint) {
case 'cards': case 'cards':
result = Card result = Card
.find(lang, req.query) .find(lang, query)
.map((c) => c.resume()) .map((c) => c.resume())
break break
case 'sets': case 'sets':
result = Set result = Set
.find(lang, req.query) .find(lang, query)
.map((c) => c.resume()) .map((c) => c.resume())
break break
case 'series': case 'series':
result = Serie result = Serie
.find(lang, req.query) .find(lang, query)
.map((c) => c.resume()) .map((c) => c.resume())
break break
case 'categories': case 'categories':
@ -95,7 +133,7 @@ server
case "suffixes": case "suffixes":
case "trainer-types": case "trainer-types":
result = unique( result = unique(
Card.raw(lang) Card.getAll(lang)
.map((c) => c[endpointToField[endpoint]] as string) .map((c) => c[endpointToField[endpoint]] as string)
.filter((c) => c) .filter((c) => c)
).sort(betterSorter) ).sort(betterSorter)
@ -103,7 +141,7 @@ server
case "types": case "types":
case "dex-ids": case "dex-ids":
result = unique( result = unique(
Card.raw(lang) Card.getAll(lang)
.map((c) => c[endpointToField[endpoint]] as Array<string>) .map((c) => c[endpointToField[endpoint]] as Array<string>)
.filter((c) => c) .filter((c) => c)
.reduce((p, c) => [...p, ...c], [] as Array<string>) .reduce((p, c) => [...p, ...c], [] as Array<string>)
@ -111,7 +149,7 @@ server
break break
case "variants": case "variants":
result = unique( result = unique(
Card.raw(lang) Card.getAll(lang)
.map((c) => objectKeys(c.variants ?? {}) as Array<string>) .map((c) => objectKeys(c.variants ?? {}) as Array<string>)
.filter((c) => c) .filter((c) => c)
.reduce((p, c) => [...p, ...c], [] as Array<string>) .reduce((p, c) => [...p, ...c], [] as Array<string>)
@ -148,23 +186,23 @@ server
let result: any | undefined let result: any | undefined
switch (endpoint) { switch (endpoint) {
case 'cards': case 'cards':
result = Card.findOne(lang, {id})?.full() result = Card.findOne(lang, { filters: { id }})?.full()
if (!result) { if (!result) {
result = Card.findOne(lang, {name: id})?.full() result = Card.findOne(lang, { filters: { name: id }})?.full()
} }
break break
case 'sets': case 'sets':
result = Set.findOne(lang, {id})?.full() result = Set.findOne(lang, { filters: { id }})?.full()
if (!result) { if (!result) {
result = Set.findOne(lang, {name: id})?.full() result = Set.findOne(lang, {filters: { name: id }})?.full()
} }
break break
case 'series': case 'series':
result = Serie.findOne(lang, {id})?.full() result = Serie.findOne(lang, { filters: { id }})?.full()
if (!result) { if (!result) {
result = Serie.findOne(lang, {name: id})?.full() result = Serie.findOne(lang, { filters: { name: id }})?.full()
} }
break break
default: default:
@ -204,7 +242,7 @@ server
switch (endpoint) { switch (endpoint) {
case 'sets': case 'sets':
result = Card result = Card
.findOne(lang, {localId: subid, set: id})?.full() .findOne(lang, { filters: { localId: subid, set: id }})?.full()
break break
} }
if (!result) { if (!result) {

View File

@ -11,7 +11,7 @@ const router = express.Router()
* Drawbacks * Drawbacks
* Attack.damage is a string instead of possibly being a number or a string * Attack.damage is a string instead of possibly being a number or a string
*/ */
const schema = buildSchema(fs.readFileSync('./public/v2/graphql.gql').toString()) const schema = buildSchema(fs.readFileSync('./public/v2/graphql.gql', 'utf-8'))
// Error Logging for debugging // Error Logging for debugging
function graphQLErrorHandle(error: GraphQLError) { function graphQLErrorHandle(error: GraphQLError) {
@ -26,19 +26,15 @@ function graphQLErrorHandle(error: GraphQLError) {
return formatError(error) return formatError(error)
} }
// Add graphql to the route const graphql = graphqlHTTP({
router.get('/', graphqlHTTP({
schema, schema,
rootValue: resolver, rootValue: resolver,
graphiql: true, graphiql: true,
customFormatErrorFn: graphQLErrorHandle customFormatErrorFn: graphQLErrorHandle
})) })
router.post('/', graphqlHTTP({ // Add graphql to the route
schema, router.get('/', graphql)
rootValue: resolver, router.post('/', graphql)
graphiql: true,
customFormatErrorFn: graphQLErrorHandle
}))
export default router export default router

View File

@ -1,19 +1,25 @@
import { SupportedLanguages } from '@tcgdex/sdk'
import { Query } from '../../interfaces'
import { checkLanguage } from '../../util'
import Card from '../Components/Card' import Card from '../Components/Card'
import { Options } from '../../interfaces'
import Serie from '../Components/Serie' import Serie from '../Components/Serie'
import Set from '../Components/Set' 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) => ( const middleware = (fn: (lang: SupportedLanguages, query: Query) => any) => (
data: Q, data: Query,
_: any, _: any,
e: any e: any
) => { ) => {
// get the locale directive // get the locale directive
const langArgument = e?.fieldNodes?.[0]?.directives?.[0]?.arguments?.[0]?.value const langArgument = e?.fieldNodes?.[0]?.directives?.[0]?.arguments?.[0]?.value
// 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 there is no locale directive // if there is no locale directive
if (!langArgument) { if (!langArgument) {
return fn('en', data) return fn('en', data)
@ -37,28 +43,27 @@ const middleware = <Q extends Record<string, any> = Record<string, any>>(fn: (la
export default { export default {
// Cards Endpoints // Cards Endpoints
cards: middleware<Options<keyof Card['card']>>((lang, query) => { cards: middleware((lang, query) => {
return Card.find(lang, query.filters ?? {}, query.pagination) return Card.find(lang, query)
}), }),
card: middleware<{set?: string, id: string}>((lang, query) => { card: middleware((lang, query) => {
const toSearch = query.set ? 'localId' : 'id' return Card.findOne(lang, query)
return Card.findOne(lang, {[toSearch]: query.id})
}), }),
// Set Endpoints // Set Endpoints
set: middleware<{id: string}>((lang, query) => { set: middleware((lang, query) => {
return Set.findOne(lang, {id: query.id}) ?? Set.findOne(lang, {name: query.id}) return Set.findOne(lang, query)
}), }),
sets: middleware<Options<keyof Set['set']>>((lang, query) => { sets: middleware((lang, query) => {
return Set.find(lang, query.filters ?? {}, query.pagination) return Set.find(lang, query)
}), }),
// Serie Endpoints // Serie Endpoints
serie: middleware<{id: string}>((lang, query) => { serie: middleware((lang, query) => {
return Serie.findOne(lang, {id: query.id}) ?? Serie.findOne(lang, {name: query.id}) return Serie.findOne(lang, query)
}), }),
series: middleware<Options<keyof Serie['serie']>>((lang, query) => { series: middleware((lang, query) => {
return Serie.find(lang, query.filters ?? {}, query.pagination) return Serie.find(lang, query)
}), }),
}; };

View File

@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import status from './status'
import jsonEndpoints from './V2/endpoints/jsonEndpoints' import jsonEndpoints from './V2/endpoints/jsonEndpoints'
import graphql from './V2/graphql' import graphql from './V2/graphql'
import status from './status'
// Current API version // Current API version
const VERSION = 2 const VERSION = 2
@ -20,19 +20,39 @@ server.use((_, res, next) => {
// Route logging / Error logging for debugging // Route logging / Error logging for debugging
server.use((req, res, next) => { server.use((req, res, next) => {
const now = new Date()
// Date of request User-Agent 32 first chars request Method
let prefix = `\x1b[2m${now.toISOString()}\x1b[22m ${req.headers['user-agent']?.slice(0, 32).padEnd(32)} ${req.method.padEnd(7)}`
const url = new URL(req.url, `http://${req.headers.host}`)
const fullURL = url.toString()
const path = fullURL.slice(fullURL.indexOf(url.pathname, fullURL.indexOf(url.host)))
// HTTP Status Code Time to run request path of request
console.log(`${prefix} ${''.padStart(5, ' ')} ${''.padStart(7, ' ')} ${path}`)
res.on('close', () => { res.on('close', () => {
console.log(`[${new Date().toISOString()}] ${req.headers['user-agent']?.slice(0, 32).padEnd(32)} ${req.method.padEnd(7)} ${res.statusCode} ${(req.baseUrl ?? '') + req.url}`) console.log(`${prefix} \x1b[34m[${'statusCode' in res ? res.statusCode : '???'}]\x1b[0m \x1b[2m${(new Date().getTime() - now.getTime()).toFixed(0).padStart(5, ' ')}ms\x1b[22m ${path}`)
}) })
res.on('error', (err) => { res.on('error', (err) => {
console.error('Error:') // log the request
console.log(`${prefix} \x1b[34m[500]\x1b[0m \x1b[2m${(new Date().getTime() - now.getTime()).toFixed(0).padStart(5, ' ')}ms\x1b[22m ${path}`)
// add a full line dash to not miss it
const columns = (process?.stdout?.columns ?? 32) - 7
const dashes = ''.padEnd(columns / 2, '-')
// colorize the lines to make sur to not miss it
console.error(`\x1b[91m${dashes} ERROR ${dashes}\x1b[0m`)
console.error(err) console.error(err)
console.error(`\x1b[91m${dashes} ERROR ${dashes}\x1b[0m`)
}) })
next() next()
}) })
server.get('/', (_, res) => { server.get('/', (_, res) => {
res.redirect('https://www.tcgdex.dev/?ref=api.tcgdex.net') res.redirect('https://www.tcgdex.dev/?ref=api.tccgdex.net')
}) })
server.use(express.static('./public')) server.use(express.static('./public'))

View File

@ -9,3 +9,62 @@ export interface Options<T> {
pagination?: Pagination pagination?: Pagination
filters?: Partial<Record<T, any>> filters?: Partial<Record<T, any>>
} }
export interface Query<T extends {} = {}> {
pagination?: {
/**
* the page number wanted
*/
page: number
/**
* the number of items per pages
*
* @default 100
*/
itemsPerPage?: number
}
/**
* Filters used in the query
*/
filters?: Partial<{ [Key in keyof T]: T[Key] extends object ? string | number | Array<string | number> : T[Key] | Array<T[Key]> }>
/**
* data sorting
*
* It automatically manage numbers sorting as to not show them using alphabet sorting
*
* @default {field: 'id', order: 'ASC'}
*/
sort?: {
field: string
order: 'ASC' | 'DESC'
}
}
export interface QueryResult<T> {
/**
* if pagination query is set it will be set
*/
pagination?: {
/**
* the current page
*/
page: number
/**
* the total number of pages
*/
pageTotal: number
/**
* the number of items per pages
*/
itemsPerPage: number
}
/**
* number of items
*/
count: number
/**
* list of items
*/
items: Array<T>
}

View File

@ -265,7 +265,7 @@ export default express.Router()
<table class="serie"> <table class="serie">
${objectMap(setsData, (serie, serieId) => { ${objectMap(setsData, (serie, serieId) => {
// Loop through every series and name them // Loop through every series and name them
const name = Serie.findOne('en', {id: serieId})?.name const name = Serie.findOne('en', { filters: { id: serieId }})?.name
return ` return `
<thead> <thead>
<tr><th class="notop" colspan="13"><h2>${name} (${serieId})</h2></th></tr> <tr><th class="notop" colspan="13"><h2>${name} (${serieId})</h2></th></tr>
@ -304,7 +304,7 @@ export default express.Router()
// loop through every sets // loop through every sets
// find the set in the first available language (Should be English globally) // find the set in the first available language (Should be English globally)
const setTotal = Set.findOne(data[0] as 'en', {id: setId}) const setTotal = Set.findOne(data[0] as 'en', { filters: { id: setId }})
let str = '<tr>' + `<td>${setTotal?.name} (${setId}) <br />${setTotal?.cardCount.total ?? 1} cards</td>` let str = '<tr>' + `<td>${setTotal?.name} (${setId}) <br />${setTotal?.cardCount.total ?? 1} cards</td>`
// Loop through every languages // Loop through every languages

View File

@ -1,5 +1,7 @@
import { objectLoop } from '@dzeio/object-util'
import { SupportedLanguages } from '@tcgdex/sdk' import { SupportedLanguages } from '@tcgdex/sdk'
import { Response } from 'express' import { Response } from 'express'
import { Query } from './interfaces'
export function checkLanguage(str: string): str is SupportedLanguages { export function checkLanguage(str: string): str is SupportedLanguages {
return ['en', 'fr', 'es', 'it', 'pt', 'de'].includes(str) return ['en', 'fr', 'es', 'it', 'pt', 'de'].includes(str)
@ -31,7 +33,6 @@ export function sendError(error: 'UnknownError' | 'NotFoundError' | 'LanguageNot
res.status(status).json({ res.status(status).json({
message message
}).end() }).end()
} }
export function betterSorter(a: string, b: string) { export function betterSorter(a: string, b: string) {
@ -54,27 +55,120 @@ export function betterSorter(a: string, b: string) {
// } // }
// } // }
export function lightCheck(source: any, item: any): boolean { /**
if (typeof source === 'undefined') { *
return typeof item === 'undefined' * @param validator the validation object
* @param value the value to validate
* @returns `true` if valid else `false`
*/
export function validateItem(validator: any | Array<any>, value: any): boolean {
if (typeof value === 'object') {
return objectLoop(value, (v) => {
// early exit to not infinitively loop through objects
if (typeof v === 'object') return true
// check for each childs
return validateItem(validator, v)
})
} }
if (Array.isArray(source)) {
for (const sub of source) { // loop to validate for an array
const res = lightCheck(sub, item) if (Array.isArray(validator)) {
for (const sub of validator) {
const res = validateItem(sub, value)
if (res) { if (res) {
return true return true
} }
} }
return false return false
} }
if (typeof source === 'object') { if (typeof validator === 'string') {
return lightCheck(source[item], true) // run a string validation
} else if (typeof source === 'string') { return value.toString().toLowerCase().includes(validator.toLowerCase())
return source.toLowerCase().includes(item.toString().toLowerCase()) } else if (typeof validator === 'number') {
} else if (typeof source === 'number') { // run a number validation
return source === parseInt(item) if (typeof value === 'number') {
return validator === value
} else {
return validator === parseFloat(value)
}
} else { } else {
// console.log(source, item) // validate if types are not conforme
return source === item return validator === value
} }
} }
/**
* @param data the data to sort
* @param query the query
* @returns the sorted data
*/
export function handleSort(data: Array<any>, query: Query<any>) {
const sort: Query<any>['sort'] = query.sort ?? {field: 'id', order: 'ASC'}
const field = sort.field
const order = sort.order ?? 'ASC'
const firstEntry = data[0]
// early exit if the order is not correctly set
if (order !== 'ASC' && order !== 'DESC') {
console.warn('Sort order is not valid', order)
return data
}
if (!(field in firstEntry)) {
return data
}
const sortType = typeof data[0][field]
if (sortType === 'number') {
if (order === 'ASC') {
return data.sort((a, b) => a[field] - b[field])
} else {
return data.sort((a, b) => b[field] - a[field])
}
} else {
if (order === 'ASC') {
return data.sort((a, b) => a[field] > b[field] ? 1 : -1)
} else {
return data.sort((a, b) => a[field] > b[field] ? -1 : 1)
}
}
}
/**
* filter data out to make it paginated
*
* @param data the data to paginate
* @param query the query
* @returns the data that is in the paginated query
*/
export function handlePagination(data: Array<any>, query: Query<any>) {
if (!query.pagination) {
return data
}
const itemsPerPage = query.pagination.itemsPerPage ?? 100
const page = query.pagination.page
// return the sliced data
return data.slice(
itemsPerPage * (page - 1),
itemsPerPage * page
)
}
/**
* filter the data using the specified query
*
* @param data the data to validate
* @param query the query to validate against
* @returns the filtered data
*/
export function handleValidation(data: Array<any>, query: Query) {
const filters = query.filters
if (!filters) {
return data
}
return data.filter((v) => objectLoop(filters, (valueToValidate, key) => {
return validateItem(valueToValidate, v[key])
}))
}