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:
parent
034b7e2cec
commit
5c8ca20a41
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@ -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
|
||||||
|
@ -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 #
|
||||||
##################
|
##################
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -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'))
|
||||||
|
59
server/src/interfaces.d.ts
vendored
59
server/src/interfaces.d.ts
vendored
@ -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>
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user