diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a14b516ead..0e4d181a77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,5 +25,8 @@ jobs: cd server bun install --frozen-lockfile - - name: Validate the data - run: bun run validate + - name: Validate the data & the server + run: | + bun run validate + cd server + bun run validate diff --git a/meta/definitions/graphql.gql b/meta/definitions/graphql.gql index 085e78d07a..45efe1a5e7 100644 --- a/meta/definitions/graphql.gql +++ b/meta/definitions/graphql.gql @@ -7,28 +7,62 @@ directive @locale ( lang: String! ) 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 { - cards(filters: CardsFilters, pagination: Pagination): [Card] - sets: [Set] - series: [Serie] + """Find the cards""" + cards(filters: CardsFilters, pagination: Pagination, sort: Sort): [Card] + + """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( id: ID!, - set: String + set: String, + """The new way to filter""" + filters: CardsFilters ): Card + + """Find one set (using the id is deprecated)""" set( - id: ID! + id: ID!, + """The new way to filter""" + filters: SetFilters ): Set + + """Find one serie (using the id is deprecated)""" serie( - id: ID! + id: ID!, + """The new way to filter""" + filters: SerieFilters ): Serie } -# Pagination input +"""Paginate the datas you fetch""" input Pagination { - page: Float! - count: Float! + """Indicate the page number (from 1)""" + 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 energyType: String evolveFrom: String - hp: Float + hp: Int id: ID localId: String - dexId: Float + dexId: Int illustrator: String image: String - level: Float + level: Int levelId: String name: String rarity: String @@ -55,7 +89,7 @@ input CardsFilters { stage: String suffix: String trainerType: String - retreat: Float + retreat: Int } type Card { @@ -63,23 +97,23 @@ type Card { attacks: [AttacksListItem] category: String! description: String - dexId: [Float] + dexId: [Int] effect: String energyType: String evolveFrom: String - hp: Float + hp: Int id: String! illustrator: String image: String item: Item legal: Legal! - level: Float + level: Int localId: String! name: String! rarity: String! regulationMark: String resistances: [WeakResListItem] - retreat: Float + retreat: Int set: Set! stage: String suffix: String @@ -143,13 +177,21 @@ type Set { tcgOnline: String } +input SetFilters { + id: String + name: String + serie: String + releaseDate: String + tcgOnline: String +} + type CardCount { - firstEd: Float - holo: Float - normal: Float - official: Float! - reverse: Float - total: Float! + firstEd: Int + holo: Int + normal: Int + official: Int! + reverse: Int + total: Int! } ################## @@ -163,6 +205,11 @@ type Serie { sets: [Set]! } +input SerieFilters { + id: String + name: String +} + ################## # StringEndpoint # ################## diff --git a/server/package.json b/server/package.json index b5fdc0c575..4690d7a282 100644 --- a/server/package.json +++ b/server/package.json @@ -5,6 +5,7 @@ "scripts": { "compile": "bun compiler/index.ts", "dev": "bun --watch --hot src/index.ts", + "validate": "tsc --noEmit --project ./tsconfig.json", "start": "bun src/index.ts" }, "license": "MIT", diff --git a/server/src/V2/Components/Card.ts b/server/src/V2/Components/Card.ts index 0bbf31772d..55fbf171ea 100644 --- a/server/src/V2/Components/Card.ts +++ b/server/src/V2/Components/Card.ts @@ -1,7 +1,7 @@ import { objectLoop } from '@dzeio/object-util' -import { Card as SDKCard, CardResume, SupportedLanguages } from '@tcgdex/sdk' -import { Pagination } from '../../interfaces' -import { lightCheck } from '../../util' +import { CardResume, Card as SDKCard, SupportedLanguages } from '@tcgdex/sdk' +import { Query } from '../../interfaces' +import { handlePagination, handleSort, handleValidation } from '../../util' import Set from './Set' type LocalCard = Omit & {set: () => Set} @@ -55,40 +55,25 @@ export default class Card implements LocalCard { }) } - 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> = {}, pagination?: Pagination) { - let list : Array = (require(`../../../generated/${lang}/cards.json`) as Array) - .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 getAll(lang: SupportedLanguages): Array { + return require(`../../../generated/${lang}/cards.json`) } - public static raw(lang: SupportedLanguages): Array { - return require(`../../../generated/${lang}/cards.json`) + public static find(lang: SupportedLanguages, query: Query) { + return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query) + .map((it) => new Card(lang, it)) } - public static findOne(lang: SupportedLanguages, params: Partial> = {}) { - const res = (require(`../../../generated/${lang}/cards.json`) as Array).find((c) => { - return objectLoop(params, (it, key) => { - if (key === 'set' && typeof it === 'string') { - return (c['set'].id === it || lightCheck(c['set'].name, it)) - } - return lightCheck(c[key as 'localId'], it) - }) - }) - if (!res) { + public static findOne(lang: SupportedLanguages, query: Query) { + const res = handleValidation(this.getAll(lang), query) + if (res.length === 0) { return undefined } - return new Card(lang, res) + return new Card(lang, res[0]) } public resume(): CardResume { diff --git a/server/src/V2/Components/Serie.ts b/server/src/V2/Components/Serie.ts index 735e018a8f..79e1524287 100644 --- a/server/src/V2/Components/Serie.ts +++ b/server/src/V2/Components/Serie.ts @@ -1,7 +1,7 @@ import { objectLoop } from '@dzeio/object-util' import { Serie as SDKSerie, SerieResume, SupportedLanguages } from '@tcgdex/sdk' -import { Pagination } from '../../interfaces' -import { lightCheck } from '../../util' +import { Query } from '../../interfaces' +import { handlePagination, handleSort, handleValidation } from '../../util' import Set from './Set' type LocalSerie = Omit & {sets: () => Array} @@ -25,34 +25,24 @@ export default class Serie implements LocalSerie { } public sets(): Array { - 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> = {}, pagination?: Pagination) { - let list = (require(`../../../generated/${lang}/series.json`) as Array) - .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 getAll(lang: SupportedLanguages): Array { + return require(`../../../generated/${lang}/series.json`) + } + + public static find(lang: SupportedLanguages, query: Query) { + return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query) + .map((it) => new Serie(lang, it)) } - public static findOne(lang: SupportedLanguages, params: Partial> = {}): Serie | undefined { - const res = (require(`../../../generated/${lang}/series.json`) as Array) - .find((c) => { - return objectLoop(params, (it, key) => { - if (key === 'id') return c[key] === it - return lightCheck(c[key as 'id'], it) - }) - }) - if (!res) { + public static findOne(lang: SupportedLanguages, query: Query) { + const res = handleValidation(this.getAll(lang), query) + if (res.length === 0) { return undefined } - return new Serie(lang, res) + return new Serie(lang, res[0]) } public resume(): SerieResume { diff --git a/server/src/V2/Components/Set.ts b/server/src/V2/Components/Set.ts index 1416d48f04..d9afbb545f 100644 --- a/server/src/V2/Components/Set.ts +++ b/server/src/V2/Components/Set.ts @@ -1,7 +1,7 @@ import { objectLoop } from '@dzeio/object-util' import { Set as SDKSet, SetResume, SupportedLanguages } from '@tcgdex/sdk' -import { Pagination } from '../../interfaces' -import { lightCheck } from '../../util' +import { Query } from '../../interfaces' +import { handlePagination, handleSort, handleValidation } from '../../util' import Card from './Card' import Serie from './Serie' @@ -39,45 +39,28 @@ export default class Set implements LocalSet { symbol?: string | undefined 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 { - 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> = {}, pagination?: Pagination) { - let list = (require(`../../../generated/${lang}/sets.json`) as Array) - .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 getAll(lang: SupportedLanguages): Array { + return require(`../../../generated/${lang}/sets.json`) } - public static findOne(lang: SupportedLanguages, params: Partial> = {}) { - const res = (require(`../../../generated/${lang}/sets.json`) as Array).find((c) => { - return 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 (!res) { + public static find(lang: SupportedLanguages, query: Query) { + return handlePagination(handleSort(handleValidation(this.getAll(lang), query), query), query) + .map((it) => new Set(lang, it)) + } + + public static findOne(lang: SupportedLanguages, query: Query) { + const res = handleValidation(this.getAll(lang), query) + if (res.length === 0) { return undefined } - return new Set(lang, res) + return new Set(lang, res[0]) } public resume(): SetResume { diff --git a/server/src/V2/endpoints/jsonEndpoints.ts b/server/src/V2/endpoints/jsonEndpoints.ts index bf6222eacb..9dd5972113 100644 --- a/server/src/V2/endpoints/jsonEndpoints.ts +++ b/server/src/V2/endpoints/jsonEndpoints.ts @@ -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 apicache from 'apicache' +import express from 'express' +import { Query } from '../../interfaces' +import { betterSorter, checkLanguage, sendError, unique } from '../../util' import Card from '../Components/Card' import Serie from '../Components/Serie' import Set from '../Components/Set' -import express from 'express' -import apicache from 'apicache' -import { betterSorter, checkLanguage, sendError, unique } from '../../util' const server = express.Router() @@ -48,6 +49,40 @@ server 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>, (value: string | Array, 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 @@ -56,6 +91,9 @@ server .get('/:lang/:endpoint', (req, res): void => { let { lang, endpoint } = req.params + // @ts-expect-error normal behavior + const query: Query = req.advQuery + if (endpoint.endsWith('.json')) { endpoint = endpoint.replace('.json', '') } @@ -69,18 +107,18 @@ server switch (endpoint) { case 'cards': result = Card - .find(lang, req.query) + .find(lang, query) .map((c) => c.resume()) break case 'sets': result = Set - .find(lang, req.query) + .find(lang, query) .map((c) => c.resume()) break case 'series': result = Serie - .find(lang, req.query) + .find(lang, query) .map((c) => c.resume()) break case 'categories': @@ -95,7 +133,7 @@ server case "suffixes": case "trainer-types": result = unique( - Card.raw(lang) + Card.getAll(lang) .map((c) => c[endpointToField[endpoint]] as string) .filter((c) => c) ).sort(betterSorter) @@ -103,7 +141,7 @@ server case "types": case "dex-ids": result = unique( - Card.raw(lang) + Card.getAll(lang) .map((c) => c[endpointToField[endpoint]] as Array) .filter((c) => c) .reduce((p, c) => [...p, ...c], [] as Array) @@ -111,7 +149,7 @@ server break case "variants": result = unique( - Card.raw(lang) + Card.getAll(lang) .map((c) => objectKeys(c.variants ?? {}) as Array) .filter((c) => c) .reduce((p, c) => [...p, ...c], [] as Array) @@ -148,23 +186,23 @@ server let result: any | undefined switch (endpoint) { case 'cards': - result = Card.findOne(lang, {id})?.full() + result = Card.findOne(lang, { filters: { id }})?.full() if (!result) { - result = Card.findOne(lang, {name: id})?.full() + result = Card.findOne(lang, { filters: { name: id }})?.full() } break case 'sets': - result = Set.findOne(lang, {id})?.full() + result = Set.findOne(lang, { filters: { id }})?.full() if (!result) { - result = Set.findOne(lang, {name: id})?.full() + result = Set.findOne(lang, {filters: { name: id }})?.full() } break case 'series': - result = Serie.findOne(lang, {id})?.full() + result = Serie.findOne(lang, { filters: { id }})?.full() if (!result) { - result = Serie.findOne(lang, {name: id})?.full() + result = Serie.findOne(lang, { filters: { name: id }})?.full() } break default: @@ -204,7 +242,7 @@ server switch (endpoint) { case 'sets': result = Card - .findOne(lang, {localId: subid, set: id})?.full() + .findOne(lang, { filters: { localId: subid, set: id }})?.full() break } if (!result) { diff --git a/server/src/V2/graphql/index.ts b/server/src/V2/graphql/index.ts index ac68d31e77..fe19359cc7 100644 --- a/server/src/V2/graphql/index.ts +++ b/server/src/V2/graphql/index.ts @@ -11,7 +11,7 @@ const router = express.Router() * Drawbacks * 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 function graphQLErrorHandle(error: GraphQLError) { @@ -26,19 +26,15 @@ function graphQLErrorHandle(error: GraphQLError) { return formatError(error) } -// Add graphql to the route -router.get('/', graphqlHTTP({ +const graphql = graphqlHTTP({ schema, rootValue: resolver, graphiql: true, customFormatErrorFn: graphQLErrorHandle -})) +}) -router.post('/', graphqlHTTP({ - schema, - rootValue: resolver, - graphiql: true, - customFormatErrorFn: graphQLErrorHandle -})) +// Add graphql to the route +router.get('/', graphql) +router.post('/', graphql) export default router diff --git a/server/src/V2/graphql/resolver.ts b/server/src/V2/graphql/resolver.ts index 69d4eb2a37..2c05f2a910 100644 --- a/server/src/V2/graphql/resolver.ts +++ b/server/src/V2/graphql/resolver.ts @@ -1,19 +1,25 @@ +import { SupportedLanguages } from '@tcgdex/sdk' +import { Query } from '../../interfaces' +import { checkLanguage } from '../../util' import Card from '../Components/Card' -import { Options } from '../../interfaces' import Serie from '../Components/Serie' import Set from '../Components/Set' -import { SupportedLanguages } from '@tcgdex/sdk' -import { checkLanguage } from '../../util' -const middleware = = Record>(fn: (lang: SupportedLanguages, query: Q) => any) => ( - data: Q, +const middleware = (fn: (lang: SupportedLanguages, query: Query) => any) => ( + data: Query, _: any, e: any ) => { - // get the locale directive 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 (!langArgument) { return fn('en', data) @@ -37,28 +43,27 @@ const middleware = = Record>(fn: (la export default { // Cards Endpoints - cards: middleware>((lang, query) => { - return Card.find(lang, query.filters ?? {}, query.pagination) + cards: middleware((lang, query) => { + return Card.find(lang, query) }), - card: middleware<{set?: string, id: string}>((lang, query) => { - const toSearch = query.set ? 'localId' : 'id' - return Card.findOne(lang, {[toSearch]: query.id}) + card: middleware((lang, query) => { + return Card.findOne(lang, query) }), // Set Endpoints - set: middleware<{id: string}>((lang, query) => { - return Set.findOne(lang, {id: query.id}) ?? Set.findOne(lang, {name: query.id}) + set: middleware((lang, query) => { + return Set.findOne(lang, query) }), - sets: middleware>((lang, query) => { - return Set.find(lang, query.filters ?? {}, query.pagination) + sets: middleware((lang, query) => { + return Set.find(lang, query) }), // Serie Endpoints - serie: middleware<{id: string}>((lang, query) => { - return Serie.findOne(lang, {id: query.id}) ?? Serie.findOne(lang, {name: query.id}) + serie: middleware((lang, query) => { + return Serie.findOne(lang, query) }), - series: middleware>((lang, query) => { - return Serie.find(lang, query.filters ?? {}, query.pagination) + series: middleware((lang, query) => { + return Serie.find(lang, query) }), }; diff --git a/server/src/index.ts b/server/src/index.ts index f5f3de5515..cb3c0e1879 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,7 +1,7 @@ import express from 'express' -import status from './status' import jsonEndpoints from './V2/endpoints/jsonEndpoints' import graphql from './V2/graphql' +import status from './status' // Current API version const VERSION = 2 @@ -20,19 +20,39 @@ server.use((_, res, next) => { // Route logging / Error logging for debugging 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', () => { - 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) => { - 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(`\x1b[91m${dashes} ERROR ${dashes}\x1b[0m`) }) next() }) 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')) diff --git a/server/src/interfaces.d.ts b/server/src/interfaces.d.ts index 6d1677d42c..c3f075c813 100644 --- a/server/src/interfaces.d.ts +++ b/server/src/interfaces.d.ts @@ -9,3 +9,62 @@ export interface Options { pagination?: Pagination filters?: Partial> } + +export interface Query { + 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 : T[Key] | Array }> + + /** + * 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 { + /** + * 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 +} diff --git a/server/src/status.ts b/server/src/status.ts index 4699998869..f3d3cf0776 100644 --- a/server/src/status.ts +++ b/server/src/status.ts @@ -265,7 +265,7 @@ export default express.Router() ${objectMap(setsData, (serie, serieId) => { // 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 ` @@ -304,7 +304,7 @@ export default express.Router() // loop through every sets // 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 = '' + `` // Loop through every languages diff --git a/server/src/util.ts b/server/src/util.ts index 4b3732134e..282d4956db 100644 --- a/server/src/util.ts +++ b/server/src/util.ts @@ -1,5 +1,7 @@ +import { objectLoop } from '@dzeio/object-util' import { SupportedLanguages } from '@tcgdex/sdk' import { Response } from 'express' +import { Query } from './interfaces' export function checkLanguage(str: string): str is SupportedLanguages { return ['en', 'fr', 'es', 'it', 'pt', 'de'].includes(str) @@ -31,7 +33,6 @@ export function sendError(error: 'UnknownError' | 'NotFoundError' | 'LanguageNot res.status(status).json({ message }).end() - } 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, 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) { - const res = lightCheck(sub, item) + + // loop to validate for an array + if (Array.isArray(validator)) { + for (const sub of validator) { + const res = validateItem(sub, value) if (res) { return true } } return false } - if (typeof source === 'object') { - return lightCheck(source[item], true) - } else if (typeof source === 'string') { - return source.toLowerCase().includes(item.toString().toLowerCase()) - } else if (typeof source === 'number') { - return source === parseInt(item) + if (typeof validator === 'string') { + // run a string validation + return value.toString().toLowerCase().includes(validator.toLowerCase()) + } else if (typeof validator === 'number') { + // run a number validation + if (typeof value === 'number') { + return validator === value + } else { + return validator === parseFloat(value) + } + } else { + // validate if types are not conforme + return validator === value + } +} + +/** + * @param data the data to sort + * @param query the query + * @returns the sorted data + */ +export function handleSort(data: Array, query: Query) { + const sort: Query['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 { - // console.log(source, item) - return source === item + 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, query: Query) { + 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, query: Query) { + const filters = query.filters + if (!filters) { + return data + } + + return data.filter((v) => objectLoop(filters, (valueToValidate, key) => { + return validateItem(valueToValidate, v[key]) + })) }

${name} (${serieId})

${setTotal?.name} (${setId})
${setTotal?.cardCount.total ?? 1} cards