mirror of
https://github.com/tcgdex/cards-database.git
synced 2025-04-22 19:02:10 +00:00
feat: Add advanced filtering capabilities (#522)
This commit is contained in:
parent
5899083e5d
commit
4700618047
25
.bruno/cards/advanced-query.bru
Normal file
25
.bruno/cards/advanced-query.bru
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
meta {
|
||||||
|
name: Advanced Query
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{BASE_URL}}/v2/en/cards?name=eq:Pikachu&hp=gte:60&hp=lt:70&localId=5&localId=not:tg&id=neq:cel25-5
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
name: eq:Pikachu
|
||||||
|
hp: gte:60
|
||||||
|
hp: lt:70
|
||||||
|
localId: 5
|
||||||
|
localId: not:tg
|
||||||
|
id: neq:cel25-5
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: eq 200
|
||||||
|
res.body: length 14
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
vars {
|
vars {
|
||||||
BASE_URL: http://localhost:3000
|
BASE_URL: http://127.0.0.1:3000
|
||||||
}
|
}
|
||||||
|
21
.bruno/sets/Advanced Query.bru
Normal file
21
.bruno/sets/Advanced Query.bru
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
meta {
|
||||||
|
name: Advanced Query
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{BASE_URL}}/v2/en/sets?cardCount.official=gt:64&id=swsh
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
cardCount.official:gt: 64
|
||||||
|
id: swsh
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
res.status: eq 200
|
||||||
|
res.body: length 17
|
||||||
|
}
|
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@ -21,6 +21,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
|
bun install -g @usebruno/cli
|
||||||
bun install --frozen-lockfile
|
bun install --frozen-lockfile
|
||||||
cd server
|
cd server
|
||||||
bun install --frozen-lockfile
|
bun install --frozen-lockfile
|
||||||
@ -31,3 +32,11 @@ jobs:
|
|||||||
bun run validate
|
bun run validate
|
||||||
cd server
|
cd server
|
||||||
bun run validate
|
bun run validate
|
||||||
|
|
||||||
|
- name: Validate some requests
|
||||||
|
run: |
|
||||||
|
cd server
|
||||||
|
bun run start &
|
||||||
|
sleep 10
|
||||||
|
cd ../.bruno
|
||||||
|
bru run --env Developpement
|
||||||
|
BIN
server/bun.lockb
BIN
server/bun.lockb
Binary file not shown.
@ -1,48 +1,47 @@
|
|||||||
import { objectLoop } from '@dzeio/object-util'
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
import { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk'
|
import type { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk'
|
||||||
import { Query } from '../../interfaces'
|
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
|
||||||
import { handlePagination, handleSort, handleValidation } from '../../util'
|
import TCGSet from './Set'
|
||||||
import Set 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 de from '../../../generated/de/cards.json'
|
||||||
import nl from '../../../generated/nl/cards.json'
|
import en from '../../../generated/en/cards.json'
|
||||||
import pl from '../../../generated/pl/cards.json'
|
import es from '../../../generated/es/cards.json'
|
||||||
import ru from '../../../generated/ru/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 ja from '../../../generated/ja/cards.json'
|
||||||
import ko from '../../../generated/ko/cards.json'
|
import ko from '../../../generated/ko/cards.json'
|
||||||
import zhtw from '../../../generated/zh-tw/cards.json'
|
import nl from '../../../generated/nl/cards.json'
|
||||||
import id from '../../../generated/id/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 th from '../../../generated/th/cards.json'
|
||||||
import zhcn from '../../../generated/zh-cn/cards.json'
|
import zhcn from '../../../generated/zh-cn/cards.json'
|
||||||
|
import zhtw from '../../../generated/zh-tw/cards.json'
|
||||||
|
|
||||||
const cards = {
|
const cards = {
|
||||||
'en': en,
|
en: en,
|
||||||
'fr': fr,
|
fr: fr,
|
||||||
'es': es,
|
es: es,
|
||||||
'it': it,
|
it: it,
|
||||||
'pt': pt,
|
pt: pt,
|
||||||
'pt-br': ptbr,
|
'pt-br': ptbr,
|
||||||
'pt-pt': ptpt,
|
'pt-pt': ptpt,
|
||||||
'de': de,
|
de: de,
|
||||||
'nl': nl,
|
nl: nl,
|
||||||
'pl': pl,
|
pl: pl,
|
||||||
'ru': ru,
|
ru: ru,
|
||||||
'ja': ja,
|
ja: ja,
|
||||||
'ko': ko,
|
ko: ko,
|
||||||
'zh-tw': zhtw,
|
'zh-tw': zhtw,
|
||||||
'id': id,
|
id: id,
|
||||||
'th': th,
|
th: th,
|
||||||
'zh-cn': zhcn,
|
'zh-cn': zhcn,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type LocalCard = Omit<SDKCard, 'set'> & {set: () => Set}
|
type LocalCard = Omit<SDKCard, 'set'> & {set: () => TCGSet}
|
||||||
|
|
||||||
interface variants {
|
interface variants {
|
||||||
normal?: boolean;
|
normal?: boolean;
|
||||||
@ -93,8 +92,8 @@ export default class Card implements LocalCard {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(): Set {
|
public set(): TCGSet {
|
||||||
return Set.findOne(this.lang, {filters: { id: this.card.set.id }}) as Set
|
return TCGSet.findOne(this.lang, { id: this.card.set.id }) as TCGSet
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getAll(lang: SupportedLanguages): Array<SDKCard> {
|
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>) {
|
public static find(lang: SupportedLanguages, query: Query<SDKCard>) {
|
||||||
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
|
return executeQuery(Card.getAll(lang), query).data.map((it) => new Card(lang, it))
|
||||||
.map((it) => new Card(lang, it))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static findOne(lang: SupportedLanguages, query: Query<SDKCard>) {
|
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) {
|
if (res.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new Card(lang, res[0])
|
return res[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): CardResume {
|
public resume(): CardResume {
|
||||||
|
@ -1,49 +1,48 @@
|
|||||||
import { objectLoop } from '@dzeio/object-util'
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
|
import type { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk'
|
||||||
import { Query } from '../../interfaces'
|
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
|
||||||
import { handlePagination, handleSort, handleValidation } from '../../util'
|
import TCGSet from './Set'
|
||||||
import Set 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 de from '../../../generated/de/series.json'
|
||||||
import nl from '../../../generated/nl/series.json'
|
import en from '../../../generated/en/series.json'
|
||||||
import pl from '../../../generated/pl/series.json'
|
import es from '../../../generated/es/series.json'
|
||||||
import ru from '../../../generated/ru/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 ja from '../../../generated/ja/series.json'
|
||||||
import ko from '../../../generated/ko/series.json'
|
import ko from '../../../generated/ko/series.json'
|
||||||
import zhtw from '../../../generated/zh-tw/series.json'
|
import nl from '../../../generated/nl/series.json'
|
||||||
import id from '../../../generated/id/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 th from '../../../generated/th/series.json'
|
||||||
import zhcn from '../../../generated/zh-cn/series.json'
|
import zhcn from '../../../generated/zh-cn/series.json'
|
||||||
|
import zhtw from '../../../generated/zh-tw/series.json'
|
||||||
|
|
||||||
|
|
||||||
const series = {
|
const series = {
|
||||||
'en': en,
|
en: en,
|
||||||
'fr': fr,
|
fr: fr,
|
||||||
'es': es,
|
es: es,
|
||||||
'it': it,
|
it: it,
|
||||||
'pt': pt,
|
pt: pt,
|
||||||
'pt-br': ptbr,
|
'pt-br': ptbr,
|
||||||
'pt-pt': ptpt,
|
'pt-pt': ptpt,
|
||||||
'de': de,
|
de: de,
|
||||||
'nl': nl,
|
nl: nl,
|
||||||
'pl': pl,
|
pl: pl,
|
||||||
'ru': ru,
|
ru: ru,
|
||||||
'ja': ja,
|
ja: ja,
|
||||||
'ko': ko,
|
ko: ko,
|
||||||
'zh-tw': zhtw,
|
'zh-tw': zhtw,
|
||||||
'id': id,
|
id: id,
|
||||||
'th': th,
|
th: th,
|
||||||
'zh-cn': zhcn,
|
'zh-cn': zhcn,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<Set>}
|
type LocalSerie = Omit<SDKSerie, 'sets'> & {sets: () => Array<TCGSet>}
|
||||||
|
|
||||||
export default class Serie implements LocalSerie {
|
export default class Serie implements LocalSerie {
|
||||||
|
|
||||||
@ -63,8 +62,8 @@ export default class Serie implements LocalSerie {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public sets(): Array<Set> {
|
public sets(): Array<TCGSet> {
|
||||||
return this.serie.sets.map((s) => Set.findOne(this.lang, {filters: { id: s.id }}) as Set)
|
return this.serie.sets.map((s) => TCGSet.findOne(this.lang, { id: s.id }) as TCGSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getAll(lang: SupportedLanguages): Array<SDKSerie> {
|
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>) {
|
public static find(lang: SupportedLanguages, query: Query<SDKSerie>) {
|
||||||
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
|
return executeQuery(Serie.getAll(lang), query).data.map((it) => new Serie(lang, it))
|
||||||
.map((it) => new Serie(lang, it))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static findOne(lang: SupportedLanguages, query: Query<SDKSerie>) {
|
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) {
|
if (res.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new Serie(lang, res[0])
|
return res[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): SerieResume {
|
public resume(): SerieResume {
|
||||||
|
@ -1,45 +1,44 @@
|
|||||||
import { objectLoop } from '@dzeio/object-util'
|
import { objectLoop } from '@dzeio/object-util'
|
||||||
import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
|
import type { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk'
|
||||||
import { Query } from '../../interfaces'
|
import { executeQuery, type Query } from '../../libs/QueryEngine/filter'
|
||||||
import { handlePagination, handleSort, handleValidation } from '../../util'
|
|
||||||
import Card from './Card'
|
import Card from './Card'
|
||||||
import Serie from './Serie'
|
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 de from '../../../generated/de/sets.json'
|
||||||
import nl from '../../../generated/nl/sets.json'
|
import en from '../../../generated/en/sets.json'
|
||||||
import pl from '../../../generated/pl/sets.json'
|
import es from '../../../generated/es/sets.json'
|
||||||
import ru from '../../../generated/ru/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 ja from '../../../generated/ja/sets.json'
|
||||||
import ko from '../../../generated/ko/sets.json'
|
import ko from '../../../generated/ko/sets.json'
|
||||||
import zhtw from '../../../generated/zh-tw/sets.json'
|
import nl from '../../../generated/nl/sets.json'
|
||||||
import id from '../../../generated/id/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 th from '../../../generated/th/sets.json'
|
||||||
import zhcn from '../../../generated/zh-cn/sets.json'
|
import zhcn from '../../../generated/zh-cn/sets.json'
|
||||||
|
import zhtw from '../../../generated/zh-tw/sets.json'
|
||||||
|
|
||||||
const sets = {
|
const sets = {
|
||||||
'en': en,
|
en: en,
|
||||||
'fr': fr,
|
fr: fr,
|
||||||
'es': es,
|
es: es,
|
||||||
'it': it,
|
it: it,
|
||||||
'pt': pt,
|
pt: pt,
|
||||||
'pt-br': ptbr,
|
'pt-br': ptbr,
|
||||||
'pt-pt': ptpt,
|
'pt-pt': ptpt,
|
||||||
'de': de,
|
de: de,
|
||||||
'nl': nl,
|
nl: nl,
|
||||||
'pl': pl,
|
pl: pl,
|
||||||
'ru': ru,
|
ru: ru,
|
||||||
'ja': ja,
|
ja: ja,
|
||||||
'ko': ko,
|
ko: ko,
|
||||||
'zh-tw': zhtw,
|
'zh-tw': zhtw,
|
||||||
'id': id,
|
id: id,
|
||||||
'th': th,
|
th: th,
|
||||||
'zh-cn': zhcn,
|
'zh-cn': zhcn,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@ -77,11 +76,11 @@ export default class Set implements LocalSet {
|
|||||||
symbol?: string | undefined
|
symbol?: string | undefined
|
||||||
|
|
||||||
public serie(): Serie {
|
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> {
|
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> {
|
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>) {
|
public static find(lang: SupportedLanguages, query: Query<SDKSet>) {
|
||||||
return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query)
|
return executeQuery(Set.getAll(lang), query).data.map((it) => new Set(lang, it))
|
||||||
.map((it) => new Set(lang, it))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static findOne(lang: SupportedLanguages, query: Query<SDKSet>) {
|
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) {
|
if (res.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new Set(lang, res[0])
|
return res[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
public resume(): SetResume {
|
public resume(): SetResume {
|
||||||
|
@ -1,41 +1,42 @@
|
|||||||
import { objectKeys, objectLoop } from '@dzeio/object-util'
|
import { objectKeys } from '@dzeio/object-util'
|
||||||
import { Card as SDKCard } from '@tcgdex/sdk'
|
import type { Card as SDKCard } from '@tcgdex/sdk'
|
||||||
import apicache from 'apicache'
|
import apicache from 'apicache'
|
||||||
import express, { Request } from 'express'
|
import express, { type Request } from 'express'
|
||||||
import { Query } from '../../interfaces'
|
|
||||||
import { Errors, sendError } from '../../libs/Errors'
|
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 { betterSorter, checkLanguage, 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 TCGSet from '../Components/Set'
|
||||||
|
|
||||||
type CustomRequest = Request & {
|
type CustomRequest = Request & {
|
||||||
/**
|
/**
|
||||||
* disable caching
|
* disable caching
|
||||||
*/
|
*/
|
||||||
DO_NOT_CACHE?: boolean
|
DO_NOT_CACHE?: boolean
|
||||||
advQuery?: Query<any>
|
advQuery?: Query
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = express.Router()
|
const server = express.Router()
|
||||||
|
|
||||||
const endpointToField: Record<string, keyof SDKCard> = {
|
const endpointToField: Record<string, keyof SDKCard> = {
|
||||||
"categories": 'category',
|
categories: 'category',
|
||||||
'energy-types': 'energyType',
|
'energy-types': 'energyType',
|
||||||
"hp": 'hp',
|
hp: 'hp',
|
||||||
'illustrators': 'illustrator',
|
illustrators: 'illustrator',
|
||||||
"rarities": 'rarity',
|
rarities: 'rarity',
|
||||||
'regulation-marks': 'regulationMark',
|
'regulation-marks': 'regulationMark',
|
||||||
"retreats": 'retreat',
|
retreats: 'retreat',
|
||||||
"stages": "stage",
|
stages: "stage",
|
||||||
"suffixes": "suffix",
|
suffixes: "suffix",
|
||||||
"trainer-types": "trainerType",
|
"trainer-types": "trainerType",
|
||||||
|
|
||||||
// fields that need special care
|
// fields that need special care
|
||||||
'dex-ids': 'dexId',
|
'dex-ids': 'dexId',
|
||||||
"sets": "set",
|
sets: "set",
|
||||||
"types": "types",
|
types: "types",
|
||||||
"variants": "variants",
|
variants: "variants",
|
||||||
}
|
}
|
||||||
|
|
||||||
server
|
server
|
||||||
@ -66,27 +67,7 @@ server
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: Query = {
|
req.advQuery = recordToQuery(req.query as Record<string, string | Array<string>>)
|
||||||
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
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
@ -102,15 +83,16 @@ server
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
const query: Query = req.advQuery!
|
const query: Query = req.advQuery!
|
||||||
|
|
||||||
let data: Array<Card | Set | Serie> = []
|
let data: Array<Card | TCGSet | Serie> = []
|
||||||
switch (what.toLowerCase()) {
|
switch (what.toLowerCase()) {
|
||||||
case 'card':
|
case 'card':
|
||||||
data = Card.find(lang, query)
|
data = Card.find(lang, query)
|
||||||
break
|
break
|
||||||
case 'set':
|
case 'set':
|
||||||
data = Set.find(lang, query)
|
data = TCGSet.find(lang, query)
|
||||||
break
|
break
|
||||||
case 'serie':
|
case 'serie':
|
||||||
data = Serie.find(lang, query)
|
data = Serie.find(lang, query)
|
||||||
@ -132,7 +114,7 @@ server
|
|||||||
.get('/:lang/:endpoint', (req: CustomRequest, res): void => {
|
.get('/:lang/:endpoint', (req: CustomRequest, res): void => {
|
||||||
let { lang, endpoint } = req.params
|
let { lang, endpoint } = req.params
|
||||||
|
|
||||||
const query: Query = req.advQuery!
|
const query: Query = req.advQuery ?? {}
|
||||||
|
|
||||||
if (endpoint.endsWith('.json')) {
|
if (endpoint.endsWith('.json')) {
|
||||||
endpoint = endpoint.replace('.json', '')
|
endpoint = endpoint.replace('.json', '')
|
||||||
@ -143,7 +125,7 @@ server
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: unknown
|
||||||
|
|
||||||
switch (endpoint) {
|
switch (endpoint) {
|
||||||
case 'cards':
|
case 'cards':
|
||||||
@ -153,7 +135,7 @@ server
|
|||||||
break
|
break
|
||||||
|
|
||||||
case 'sets':
|
case 'sets':
|
||||||
result = Set
|
result = TCGSet
|
||||||
.find(lang, query)
|
.find(lang, query)
|
||||||
.map((c) => c.resume())
|
.map((c) => c.resume())
|
||||||
break
|
break
|
||||||
@ -169,7 +151,6 @@ server
|
|||||||
case "rarities":
|
case "rarities":
|
||||||
case "regulation-marks":
|
case "regulation-marks":
|
||||||
case "retreats":
|
case "retreats":
|
||||||
case "series":
|
|
||||||
case "stages":
|
case "stages":
|
||||||
case "suffixes":
|
case "suffixes":
|
||||||
case "trainer-types":
|
case "trainer-types":
|
||||||
@ -224,26 +205,26 @@ server
|
|||||||
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
|
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: any | undefined
|
let result: unknown
|
||||||
switch (endpoint) {
|
switch (endpoint) {
|
||||||
case 'cards':
|
case 'cards':
|
||||||
result = Card.findOne(lang, { filters: { id }, strict: true })?.full()
|
result = Card.findOne(lang, { id })?.full()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = Card.findOne(lang, { filters: { name: id }, strict: true })?.full()
|
result = Card.findOne(lang, { name: id })?.full()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'sets':
|
case 'sets':
|
||||||
result = Set.findOne(lang, { filters: { id }, strict: true })?.full()
|
result = TCGSet.findOne(lang, { id })?.full()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = Set.findOne(lang, {filters: { name: id }, strict: true })?.full()
|
result = TCGSet.findOne(lang, { name: id })?.full()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'series':
|
case 'series':
|
||||||
result = Serie.findOne(lang, { filters: { id }, strict: true })?.full()
|
result = Serie.findOne(lang, { id })?.full()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = Serie.findOne(lang, { filters: { name: id }, strict: true })?.full()
|
result = Serie.findOne(lang, { name: id })?.full()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -282,12 +263,14 @@ server
|
|||||||
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
|
return sendError(Errors.LANGUAGE_INVALID, res, { lang })
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: any | undefined
|
let result: unknown
|
||||||
|
|
||||||
switch (endpoint) {
|
switch (endpoint) {
|
||||||
case 'sets':
|
case 'sets':
|
||||||
|
// allow the dev to use a non prefixed value like `10` instead of `010` for newer sets
|
||||||
result = Card
|
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
|
break
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import fs from 'fs'
|
import fs from 'node:fs'
|
||||||
import { buildSchema, GraphQLError } from 'graphql'
|
import { buildSchema, type GraphQLError } from 'graphql'
|
||||||
import { createHandler } from 'graphql-http/lib/use/express'
|
import { createHandler } from 'graphql-http/lib/use/express'
|
||||||
import { type ruruHTML as RuruHTML } from 'ruru/dist/server'
|
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) */
|
/** @ts-expect-error typing is not correctly mapped (real type at ruru/dist/server.d.ts) */
|
||||||
|
@ -1,44 +1,54 @@
|
|||||||
import { SupportedLanguages } from '@tcgdex/sdk'
|
import type { SupportedLanguages } from '@tcgdex/sdk'
|
||||||
import { Query } from '../../interfaces'
|
import { type Query, Sort } from '../../libs/QueryEngine/filter'
|
||||||
|
import { recordToQuery } from '../../libs/QueryEngine/parsers'
|
||||||
import { checkLanguage } from '../../util'
|
import { checkLanguage } 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'
|
||||||
|
|
||||||
const middleware = (fn: (lang: SupportedLanguages, query: Query) => any) => (
|
// TODO: make a better way to find the language
|
||||||
data: Query,
|
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,
|
_: any,
|
||||||
e: any
|
e: any
|
||||||
) => {
|
) => {
|
||||||
// get the locale directive
|
// 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
|
// Deprecated code handling
|
||||||
// @ts-expect-error count is deprectaed in the frontend
|
if (data.pagination) {
|
||||||
if (data.pagination?.count) {
|
query.$page = data.pagination.page ?? 1
|
||||||
// @ts-expect-error count is deprectaed in the frontend
|
query.$limit = data.pagination.itemsPerPage ?? 100
|
||||||
data.pagination.itemsPerPage = data.pagination.count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no locale directive
|
|
||||||
if (!langArgument) {
|
|
||||||
return fn('en', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default locale directive value
|
if (data.sort) {
|
||||||
let lang = 'en'
|
query.$sort = {
|
||||||
|
[data.sort.field]: data.sort.order === 'DESC' ? Sort.DESC : Sort.ASC
|
||||||
// handle variable for directive value
|
}
|
||||||
if (langArgument.kind === 'Variable') {
|
|
||||||
lang = e.variableValues[langArgument.name.value]
|
|
||||||
} else {
|
|
||||||
lang = langArgument.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkLanguage(lang)) {
|
if (!checkLanguage(lang)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return fn(lang, data)
|
|
||||||
|
return fn(lang, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
463
server/src/libs/QueryEngine/filter.ts
Normal file
463
server/src/libs/QueryEngine/filter.ts
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
import { objectGet, objectKeys, objectLoop, objectSize } from '@dzeio/object-util'
|
||||||
|
import { isNull } from '../../util'
|
||||||
|
|
||||||
|
interface QueryRootFilters<Obj extends object> {
|
||||||
|
/**
|
||||||
|
* one of the results should be true to be true
|
||||||
|
*/
|
||||||
|
$or?: Array<QueryList<Obj>>
|
||||||
|
/**
|
||||||
|
* every results should be false to be true
|
||||||
|
*/
|
||||||
|
$nor?: Array<QueryList<Obj>>
|
||||||
|
/**
|
||||||
|
* (default) make sure every sub queries return true
|
||||||
|
*/
|
||||||
|
$and?: Array<QueryList<Obj>>
|
||||||
|
/**
|
||||||
|
* at least one result must be false
|
||||||
|
*/
|
||||||
|
$nand?: Array<QueryList<Obj>>
|
||||||
|
/**
|
||||||
|
* invert the result from the following query
|
||||||
|
*/
|
||||||
|
$not?: QueryList<Obj>
|
||||||
|
|
||||||
|
/**************
|
||||||
|
* PAGINATION *
|
||||||
|
**************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* define a precise offset of the data you fetched
|
||||||
|
*/
|
||||||
|
$offset?: number
|
||||||
|
/**
|
||||||
|
* limit the number of elements returned from the dataset
|
||||||
|
*/
|
||||||
|
$limit?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* instead of using a precise offset, use a page system
|
||||||
|
*/
|
||||||
|
$page?: number
|
||||||
|
|
||||||
|
/**********
|
||||||
|
* Sorting *
|
||||||
|
**********/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sort the data the way you want with each keys being priorized
|
||||||
|
*
|
||||||
|
* ex:
|
||||||
|
* {a: Sort.DESC, b: Sort.ASC}
|
||||||
|
*
|
||||||
|
* will sort first by a and if equal will sort by b
|
||||||
|
*/
|
||||||
|
$sort?: SortInterface<Obj>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical operators that can be used to filter data
|
||||||
|
*/
|
||||||
|
export type QueryLogicalOperator<Value> = {
|
||||||
|
/**
|
||||||
|
* one of the results should be true to be true
|
||||||
|
*/
|
||||||
|
$or: Array<QueryValues<Value>>
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* every results should be false to be true
|
||||||
|
*/
|
||||||
|
$nor: Array<QueryValues<Value>>
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* at least one result must be false
|
||||||
|
*/
|
||||||
|
$nand: Array<QueryValues<Value>>
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* (default) make sure every sub queries return true
|
||||||
|
*/
|
||||||
|
$and: Array<QueryValues<Value>>
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* invert the result from the following query
|
||||||
|
*/
|
||||||
|
$not: QueryValues<Value>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* differents comparisons operators that can be used to filter data
|
||||||
|
*/
|
||||||
|
export type QueryComparisonOperator<Value> = {
|
||||||
|
/**
|
||||||
|
* the remote source value must be absolutelly equal to the proposed value
|
||||||
|
*/
|
||||||
|
$eq: Value | null
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* the remote source value must be greater than the proposed value
|
||||||
|
*/
|
||||||
|
$gt: number | Date
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* the remote source value must be lesser than the proposed value
|
||||||
|
*/
|
||||||
|
$lt: number | Date
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* the remote source value must be greater or equal than the proposed value
|
||||||
|
*/
|
||||||
|
$gte: number | Date
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* the remote source value must be lesser or equal than the proposed value
|
||||||
|
*/
|
||||||
|
$lte: number | Date
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* the remote source value must be one of the proposed values
|
||||||
|
*/
|
||||||
|
$in: Array<Value>
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* laxist validation of the remote value
|
||||||
|
*
|
||||||
|
* for strings: remote contains value while not following casing like ($lax) `pou` === `Pouet` (remote)
|
||||||
|
* for numbers: it does a string conversion first
|
||||||
|
*/
|
||||||
|
$lax: Value | null
|
||||||
|
} | {
|
||||||
|
/**
|
||||||
|
* (for arrays only) specify a needed length of the array
|
||||||
|
*/
|
||||||
|
$len: number | { $gt: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryList<Obj extends object> = {
|
||||||
|
[Key in keyof Obj]?: QueryValues<Obj[Key]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Differents values the element can take
|
||||||
|
* if null it will check if it is NULL on the remote
|
||||||
|
* if array it will check oneOf
|
||||||
|
* if RegExp it will check if regexp match
|
||||||
|
*/
|
||||||
|
export type QueryValues<Value> = Value |
|
||||||
|
null |
|
||||||
|
Array<Value> |
|
||||||
|
RegExp |
|
||||||
|
QueryComparisonOperator<Value> |
|
||||||
|
QueryLogicalOperator<Value>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query element that allows you to query different elements
|
||||||
|
*/
|
||||||
|
export type Query<Obj extends object = object> = QueryList<Obj> & QueryRootFilters<Obj>
|
||||||
|
|
||||||
|
|
||||||
|
// biome-ignore lint/style/useEnumInitializers: <explanation>
|
||||||
|
export enum Sort {
|
||||||
|
/**
|
||||||
|
* Sort the values from the lowest to the largest
|
||||||
|
*/
|
||||||
|
ASC,
|
||||||
|
/**
|
||||||
|
* Sort the values form the largest to the lowest
|
||||||
|
*/
|
||||||
|
DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sorting interface with priority
|
||||||
|
*/
|
||||||
|
export type SortInterface<Obj extends object> = {
|
||||||
|
[Key in keyof Obj]?: Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export declare type AllowedValues = string | number | bigint | boolean | null | undefined
|
||||||
|
|
||||||
|
interface FilterResult<T extends object> {
|
||||||
|
data: Array<T>
|
||||||
|
rows: number
|
||||||
|
pagination?: {
|
||||||
|
page: number
|
||||||
|
pageCount: number
|
||||||
|
totalRows: number
|
||||||
|
} | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param data the original data
|
||||||
|
* @param query the query to filter against
|
||||||
|
* @param options additionnal execution options
|
||||||
|
* @returns the filtered/ordered/paginated {@link data}
|
||||||
|
*/
|
||||||
|
export function executeQuery<T extends object = Record<string, unknown>>(data: Array<T>, query: Query<T>, options?: { debug?: boolean }): FilterResult<T> {
|
||||||
|
if (options?.debug) {
|
||||||
|
console.log('Query', query)
|
||||||
|
}
|
||||||
|
// filter
|
||||||
|
let filtered = data.filter((it) => {
|
||||||
|
const res = objectLoop(query, (value, key) => {
|
||||||
|
if (key === '$or') {
|
||||||
|
for (const sub of value as Array<QueryList<T>>) {
|
||||||
|
const final = filterEntry(sub, it)
|
||||||
|
// eslint-disable-next-line max-depth
|
||||||
|
if (final) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ((key as string).startsWith('$')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return filterEntry(query, it)
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options?.debug) {
|
||||||
|
console.log('postFilters', filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort
|
||||||
|
if (query.$sort && objectSize(query.$sort) >= 1) {
|
||||||
|
// temp until better solution is found
|
||||||
|
// get the first key
|
||||||
|
const firstKey = objectKeys(query.$sort)[0]
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: item is not null
|
||||||
|
const first = query.$sort[firstKey]!
|
||||||
|
|
||||||
|
// forst by the first key
|
||||||
|
filtered = filtered.sort((objA, objB) => {
|
||||||
|
const a = objA[firstKey]
|
||||||
|
const b = objB[firstKey]
|
||||||
|
const ascend = first !== Sort.DESC // it is Ascend by default, so compare against it
|
||||||
|
if (typeof a === 'number' && typeof b === 'number') {
|
||||||
|
if (ascend) {
|
||||||
|
return b - a
|
||||||
|
}
|
||||||
|
return a - b
|
||||||
|
}
|
||||||
|
if (a instanceof Date && b instanceof Date) {
|
||||||
|
if (ascend) {
|
||||||
|
return a.getTime() - b.getTime()
|
||||||
|
}
|
||||||
|
return b.getTime() - a.getTime()
|
||||||
|
}
|
||||||
|
if (typeof a === 'string' && typeof b === 'string') {
|
||||||
|
if (ascend) {
|
||||||
|
return a.localeCompare(b)
|
||||||
|
}
|
||||||
|
return b.localeCompare(a)
|
||||||
|
|
||||||
|
}
|
||||||
|
if (ascend) {
|
||||||
|
return a > b ? 1 : -1
|
||||||
|
}
|
||||||
|
return a > b ? -1 : 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (options?.debug) {
|
||||||
|
console.log('postSort', filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// length of the query assuming a single page
|
||||||
|
const unpaginatedLength = filtered.length
|
||||||
|
let page: number | null = null
|
||||||
|
let pageCount: number | null = null
|
||||||
|
// limit
|
||||||
|
if (!isNull(query.$offset) || !isNull(query.$limit) || !isNull(query.$page)) {
|
||||||
|
let limit = query.$limit ?? -1
|
||||||
|
if (!isNull(query.$page) && isNull(query.$offset) && isNull(query.$limit)) {
|
||||||
|
console.warn('using $page NEED a $limit too, setting limit to `10`')
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
// when using $page, they start at 1 and not 0
|
||||||
|
const offset = query.$offset ?? (query.$page ? (query.$page - 1) * limit : 0)
|
||||||
|
filtered = filtered.slice(offset, limit >= 0 ? offset + limit : undefined)
|
||||||
|
page = Math.trunc(offset / limit)
|
||||||
|
pageCount = Math.ceil(unpaginatedLength / limit)
|
||||||
|
}
|
||||||
|
if (options?.debug) {
|
||||||
|
console.log('postLimit', filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: filtered,
|
||||||
|
rows: filtered.length,
|
||||||
|
pagination: (!isNull(page) && !isNull(pageCount)) ? {
|
||||||
|
page: page,
|
||||||
|
pageCount: pageCount,
|
||||||
|
totalRows: unpaginatedLength
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param query the query of the entry
|
||||||
|
* @param item the implementation of the item
|
||||||
|
* @returns if it should be kept or not
|
||||||
|
*/
|
||||||
|
export function filterEntry<T extends object>(query: QueryList<T>, item: T): boolean {
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
const res = objectLoop(query as any, (queryValue, key: keyof typeof query) => {
|
||||||
|
/**
|
||||||
|
* TODO: handle $keys
|
||||||
|
*/
|
||||||
|
if ((key as string).startsWith('$')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: unknown = undefined
|
||||||
|
|
||||||
|
// handle deeply nested items
|
||||||
|
if ((key as string).includes('.')) {
|
||||||
|
value = objectGet(item, key as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle if nested item does not exists
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
value = item[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterValue(value, queryValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* indicate if a value should be kept by an ENTIRE query
|
||||||
|
*
|
||||||
|
* @param value the value to filter
|
||||||
|
* @param query the full query
|
||||||
|
* @returns if the query should keep the value or not
|
||||||
|
*/
|
||||||
|
function filterValue<T extends AllowedValues>(value: unknown, query: QueryValues<T>) {
|
||||||
|
if (typeof query !== 'object' || query === null || query instanceof RegExp || Array.isArray(query)) {
|
||||||
|
return filterItem(value, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through each keys of the query
|
||||||
|
// eslint-disable-next-line arrow-body-style
|
||||||
|
return objectLoop(query as any, (querySubValue: unknown, queryKey: string) => {
|
||||||
|
return filterItem(value, {[queryKey]: querySubValue } as QueryValues<T>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param value the value to check
|
||||||
|
* @param query a SINGLE query to check against
|
||||||
|
* @returns if the value should be kept or not
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
function filterItem(value: any, query: QueryValues<AllowedValues>): boolean {
|
||||||
|
/**
|
||||||
|
* check if the value is null
|
||||||
|
*/
|
||||||
|
if (query === null) {
|
||||||
|
return typeof value === 'undefined' || value === null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query instanceof RegExp) {
|
||||||
|
return query.test(typeof value === 'string' ? value : value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ?!?
|
||||||
|
*/
|
||||||
|
if (value === null || typeof value === 'undefined') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* strict value check by default
|
||||||
|
*/
|
||||||
|
if (!(typeof query === 'object')) {
|
||||||
|
return query === value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array checking and $in
|
||||||
|
*/
|
||||||
|
if (Array.isArray(query) || '$in' in query) {
|
||||||
|
const arr = Array.isArray(query) ? query : query.$in as Array<AllowedValues>
|
||||||
|
return arr.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$inc' in query) {
|
||||||
|
if (typeof value === 'number' && typeof query.$inc === 'number') {
|
||||||
|
return value === query.$inc
|
||||||
|
}
|
||||||
|
return (value.toString() as string).toLowerCase().includes(query.$inc!.toString()!.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$eq' in query) {
|
||||||
|
return query.$eq === value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* numbers specific cases for numbers
|
||||||
|
*/
|
||||||
|
if ('$gt' in query) {
|
||||||
|
value = value instanceof Date ? value.getTime() : value
|
||||||
|
const comparedValue = query.$gt instanceof Date ? query.$gt.getTime() : query.$gt
|
||||||
|
return typeof value === 'number' && typeof comparedValue === 'number' && value > comparedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$lt' in query) {
|
||||||
|
value = value instanceof Date ? value.getTime() : value
|
||||||
|
const comparedValue = query.$lt instanceof Date ? query.$lt.getTime() : query.$lt
|
||||||
|
return typeof value === 'number' && typeof comparedValue === 'number' && value < comparedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$gte' in query) {
|
||||||
|
value = value instanceof Date ? value.getTime() : value
|
||||||
|
const comparedValue = query.$gte instanceof Date ? query.$gte.getTime() : query.$gte
|
||||||
|
return typeof value === 'number' && typeof comparedValue === 'number' && value >= comparedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$lte' in query) {
|
||||||
|
value = value instanceof Date ? value.getTime() : value
|
||||||
|
const comparedValue = query.$lte instanceof Date ? query.$lte.getTime() : query.$lte
|
||||||
|
return typeof value === 'number' && typeof comparedValue === 'number' && value <= comparedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$len' in query && Array.isArray(value)) {
|
||||||
|
return value.length === query.$len
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical Operators
|
||||||
|
*/
|
||||||
|
if ('$or' in query && Array.isArray(query.$or)) {
|
||||||
|
return !!query.$or.find((it) => filterValue(value, it as QueryValues<any>))
|
||||||
|
}
|
||||||
|
if ('$and' in query && Array.isArray(query.$and)) {
|
||||||
|
return !query.$and.find((it) => !filterValue(value, it as QueryValues<any>))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$not' in query) {
|
||||||
|
return !filterValue(value, query.$not as QueryValues<any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$nor' in query && Array.isArray(query.$nor)) {
|
||||||
|
return !query.$nor.find((it) => filterValue(value, it as QueryValues<any>))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('$nand' in query && Array.isArray(query.$nand)) {
|
||||||
|
return !!query.$nand.find((it) => !filterValue(value, it as QueryValues<any>))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
195
server/src/libs/QueryEngine/parsers.ts
Normal file
195
server/src/libs/QueryEngine/parsers.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { isObject, objectLoop } from '@dzeio/object-util'
|
||||||
|
import { Sort, type Query, type QueryValues } from './filter'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of allowed prefixes
|
||||||
|
*/
|
||||||
|
const prefixes = [
|
||||||
|
'like',
|
||||||
|
'not',
|
||||||
|
'notlike',
|
||||||
|
'eq',
|
||||||
|
'neq',
|
||||||
|
'gte',
|
||||||
|
'gt',
|
||||||
|
'lt',
|
||||||
|
'lte',
|
||||||
|
'null',
|
||||||
|
'notnull'
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type Prefix = typeof prefixes[number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* indicate if the string is a prefix or not
|
||||||
|
*
|
||||||
|
* @param str the string to check
|
||||||
|
* @returns {boolean} true if it's a prefix, else false
|
||||||
|
*/
|
||||||
|
function isPrefix(str: string): str is Prefix {
|
||||||
|
return prefixes.includes(str as Prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a {@link URL.searchParams} object into a {@link Query}
|
||||||
|
*
|
||||||
|
* @param searchParams the searchparams object to parse
|
||||||
|
* @param skip keys that are skipped by the transformer
|
||||||
|
*
|
||||||
|
* @returns the searchParams into a Query object
|
||||||
|
*/
|
||||||
|
export function parseSearchParams<T extends object = object>(searchParams: URLSearchParams, skip: Array<string> = []): Query<T> {
|
||||||
|
const query: Query<Record<string, unknown>> = {}
|
||||||
|
skip.push('sort:field', 'sort:order')
|
||||||
|
|
||||||
|
const sortField = searchParams.get('sort:field')
|
||||||
|
if (sortField) {
|
||||||
|
const order = searchParams.get('sort:order') ?? 'ASC'
|
||||||
|
|
||||||
|
query.$sort = { [sortField]: order === 'ASC' ? Sort.ASC : Sort.DESC }
|
||||||
|
}
|
||||||
|
for (const [key, value] of searchParams) {
|
||||||
|
|
||||||
|
if (key === 'pagination:page') {
|
||||||
|
query.$page = Number.parseInt(value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'pagination:itemsPerPage') {
|
||||||
|
query.$limit = Number.parseInt(value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip.includes(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = parseParam(key, value)
|
||||||
|
if (!query[key]) {
|
||||||
|
query[key] = params
|
||||||
|
} else {
|
||||||
|
if (isObject(params)) {
|
||||||
|
objectLoop(params, (v, k) => {
|
||||||
|
(query[key] as any)[k] = v
|
||||||
|
return
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query[key] = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(query)
|
||||||
|
return query as Query<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse a simple {@link Record} object into a {@link Query}
|
||||||
|
*
|
||||||
|
* @param searchParams the searchparams object to parse
|
||||||
|
* @param skip keys that are skipped by the transformer
|
||||||
|
*
|
||||||
|
* @returns the searchParams into a Query object
|
||||||
|
*/
|
||||||
|
export function recordToQuery<T extends object = object>(input: Record<string, string | Array<string>>, skip: Array<string> = []): Query<T> {
|
||||||
|
const query: Query<Record<string, unknown>> = {}
|
||||||
|
skip.push('sort:field', 'sort:order')
|
||||||
|
|
||||||
|
const sortField = input['sort:field'] as string
|
||||||
|
if (sortField) {
|
||||||
|
const order = input['sort:order'] ?? 'ASC'
|
||||||
|
|
||||||
|
query.$sort = { [sortField]: order === 'ASC' ? Sort.ASC : Sort.DESC }
|
||||||
|
}
|
||||||
|
|
||||||
|
objectLoop(input, (value: string | Array<string>, key) => {
|
||||||
|
|
||||||
|
if (key === 'pagination:page') {
|
||||||
|
query.$page = Number.parseInt(value as string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'pagination:itemsPerPage') {
|
||||||
|
query.$limit = Number.parseInt(value as string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip.includes(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
value = [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const it of value) {
|
||||||
|
const params = parseParam(key, it)
|
||||||
|
if (!query[key]) {
|
||||||
|
query[key] = params
|
||||||
|
} else {
|
||||||
|
if (isObject(params)) {
|
||||||
|
objectLoop(params, (v, k) => {
|
||||||
|
(query[key] as any)[k] = v
|
||||||
|
return
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query[key] = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(query)
|
||||||
|
return query as Query<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single element
|
||||||
|
*
|
||||||
|
* @param _key currently unused, kept for future compatibility
|
||||||
|
* @param value the value to parse
|
||||||
|
*
|
||||||
|
* @returns the parsed {@link Query} element to be added
|
||||||
|
*/
|
||||||
|
function parseParam(_key: string, value: string): QueryValues<unknown> {
|
||||||
|
const colonLocation = value.indexOf(':')
|
||||||
|
let filter: Prefix = 'like'
|
||||||
|
let compared: string | number = value
|
||||||
|
if (colonLocation >= 2) { // 2 because the smallest prefix is two characters long
|
||||||
|
const prefix = value.slice(0, colonLocation)
|
||||||
|
if (isPrefix(prefix)) {
|
||||||
|
filter = prefix
|
||||||
|
compared = value.slice(colonLocation + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+\.?\d*$/g.test(compared)) {
|
||||||
|
compared = Number.parseFloat(compared)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (filter) {
|
||||||
|
case 'not':
|
||||||
|
case 'notlike':
|
||||||
|
return { $not: { $inc: compared }}
|
||||||
|
case 'eq':
|
||||||
|
return compared
|
||||||
|
case 'neq':
|
||||||
|
return { $not: compared }
|
||||||
|
case 'gte':
|
||||||
|
return { $gte: compared }
|
||||||
|
case 'gt':
|
||||||
|
return { $gt: compared }
|
||||||
|
case 'lt':
|
||||||
|
return { $lt: compared }
|
||||||
|
case 'lte':
|
||||||
|
return { $lte: compared }
|
||||||
|
case 'null':
|
||||||
|
return null
|
||||||
|
case 'notnull':
|
||||||
|
return { $not: null }
|
||||||
|
default:
|
||||||
|
return { $inc: compared }
|
||||||
|
}
|
||||||
|
}
|
@ -346,7 +346,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', { filters: { id: serieId }})?.name ?? Serie.findOne('ja' as any, { filters: { id: serieId }})?.name
|
const name = Serie.findOne('en', { id: serieId })?.name ?? Serie.findOne('ja' as any, { id: serieId })?.name
|
||||||
return `
|
return `
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th class="notop" colspan="35"><h2>${name} (${serieId})</h2></th></tr>
|
<tr><th class="notop" colspan="35"><h2>${name} (${serieId})</h2></th></tr>
|
||||||
@ -364,7 +364,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', { filters: { id: setId }})
|
const setTotal = Set.findOne(data[0] as 'en', { 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>`
|
||||||
// let str = '<tr>' + `<td>${setId})</td>`
|
// let str = '<tr>' + `<td>${setId})</td>`
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "./node_modules/@dzeio/config/tsconfig.base.json",
|
"extends": "./node_modules/@dzeio/config/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user