diff --git a/meta/definitions/openapi.yaml b/meta/definitions/openapi.yaml index 9c84c04cbf..ed23173d1c 100644 --- a/meta/definitions/openapi.yaml +++ b/meta/definitions/openapi.yaml @@ -61,6 +61,7 @@ paths: operationId: cards parameters: - $ref: '#/components/parameters/filter' + - $ref: '#/components/parameters/anyName' - $ref: '#/components/parameters/sortField' - $ref: '#/components/parameters/sortOrder' - $ref: '#/components/parameters/paginationPage' @@ -1001,6 +1002,28 @@ components: type: string example: "{\"name\": \"eq:Furret\", \"hp\": \"lte:60\"}" + anyName: + name: anyName + in: query + required: false + description: | + Search cards by name across all languages. + + Unlike the `name` filter which only matches the name in the requested language, + `anyName` searches every language's card names and returns the matching cards + in the requested language. + + Supports the same operators as other filters: + - Default/`like:` - Contains match (case insensitive) + - `eq:` - Exact match + - `not:`/`notlike:` - Does not contain + - `neq:` - Not equal + + Example: searching `anyName=Dracaufeu` on `/v2/en/cards` returns Charizard. + schema: + type: [string, null] + example: "Dracaufeu" + sortField: name: "sort:field" in: query diff --git a/server/src/V2/Components/Card.ts b/server/src/V2/Components/Card.ts index bc68240690..519e361a99 100644 --- a/server/src/V2/Components/Card.ts +++ b/server/src/V2/Components/Card.ts @@ -22,7 +22,7 @@ import zhcn from '../../../generated/zh-cn/cards.json' import zhtw from '../../../generated/zh-tw/cards.json' import { getCardMarketPrice } from '../../libs/providers/cardmarket' import { getTCGPlayerPrice } from '../../libs/providers/tcgplayer' -import { executeQuery, type Query } from '../../libs/QueryEngine/filter' +import { executeQuery, type Query, type QueryValues } from '../../libs/QueryEngine/filter' // any is CompiledCard that is currently not mapped correctly const list: Record<`${string | any}${SupportedLanguages | string}`, any> = {} @@ -148,6 +148,27 @@ export async function findCards(lang: SupportedLanguages, query: Query) return executeQuery(await getAllCards(lang), query).data } +/** + * Returns the set of card IDs (lowercased) whose name matches the given query value + * in ANY language's compiled card list. + * + * This allows callers to search by a card's name regardless of which language + * the name was provided in (e.g. searching "Dracaufeu" in /v2/en/cards still + * returns Charizard). + */ +export function getMatchingIdsByAnyName(nameQueryValue: QueryValues): Set { + const matchingIds = new Set() + + for (const langCards of Object.values(cards)) { + const matched = executeQuery(langCards as Array<{ id: string; name: string }>, { name: nameQueryValue }) + for (const card of matched.data) { + matchingIds.add(card.id.toLowerCase()) + } + } + + return matchingIds +} + export async function findOneCard(lang: SupportedLanguages, query: Query) { const res = await findCards(lang, query) if (res.length === 0) { diff --git a/server/src/V2/endpoints/jsonEndpoints.ts b/server/src/V2/endpoints/jsonEndpoints.ts index 31f9062608..fff85cb8a8 100644 --- a/server/src/V2/endpoints/jsonEndpoints.ts +++ b/server/src/V2/endpoints/jsonEndpoints.ts @@ -6,7 +6,7 @@ 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 { getAllCards, findOneCard, findCards, toBrief, getCardById, getCompiledCard } from '../Components/Card' +import { getAllCards, findOneCard, findCards, toBrief, getCardById, getCompiledCard, getMatchingIdsByAnyName } from '../Components/Card' import { findOneSet, findSets, setToBrief } from '../Components/Set' import { findOneSerie, findSeries, serieToBrief } from '../Components/Serie' import { listSKUs } from '../../libs/providers/tcgplayer' @@ -130,6 +130,13 @@ server switch (endpoint) { case 'cards': { + if ('anyName' in query) { + const anyNameQuery = query.anyName + delete query.anyName + const matchingIds = Array.from(getMatchingIdsByAnyName(anyNameQuery as any)) + // @ts-expect-error id is not in the SDKCard query type but works at runtime + query.id = { $in: matchingIds } + } if ('set' in query) { const tmp = query.set delete query.set