@@ -41,12 +41,13 @@ import { In, Not, Repository } from "typeorm";
4141import { FileInterceptor } from "@nestjs/platform-express" ;
4242import bytes from "bytes" ;
4343import { isArray } from "lodash" ;
44- import { FilterSuffix } from "nestjs-paginate/lib/filter" ;
44+ import { FilterSuffix , addFilter } from "nestjs-paginate/lib/filter" ;
4545import configuration from "../../configuration" ;
4646import { DisableApiIf } from "../../decorators/disable-api-if.decorator" ;
4747import { MinimumRole } from "../../decorators/minimum-role.decorator" ;
4848import { PaginateQueryOptions } from "../../decorators/pagination.decorator" ;
4949import { ApiOkResponsePaginated , toFindOptionsRelations } from "../../globals" ;
50+ import { GameMetadata } from "../metadata/games/game.metadata.entity" ;
5051import { OtpService } from "../otp/otp.service" ;
5152import { State } from "../progresses/models/state.enum" ;
5253import { Progress } from "../progresses/progress.entity" ;
@@ -59,6 +60,15 @@ import { GamevaultGame } from "./gamevault-game.entity";
5960import { GameIdDto } from "./models/game-id.dto" ;
6061import { UpdateGameDto } from "./models/update-game.dto" ;
6162
63+ const metadataRelationNameFilters = {
64+ "metadata.genres.name" : "genres.name" ,
65+ "metadata.tags.name" : "tags.name" ,
66+ "metadata.developers.name" : "developers.name" ,
67+ "metadata.publishers.name" : "publishers.name" ,
68+ } as const ;
69+
70+ type MetadataRelationNameFilterKey = keyof typeof metadataRelationNameFilters ;
71+
6272@ApiBearerAuth ( )
6373@ApiTags ( "game" )
6474@Controller ( "games" )
@@ -224,6 +234,14 @@ export class GamesController {
224234 }
225235 }
226236
237+ const relationFilteredGameIds =
238+ await this . resolveMetadataRelationNameFilterGameIds ( query ) ;
239+
240+ if ( relationFilteredGameIds ) {
241+ this . removeMetadataRelationNameFilters ( query ) ;
242+ this . applyResolvedGameIdFilter ( query , relationFilteredGameIds ) ;
243+ }
244+
227245 if (
228246 configuration . PARENTAL . AGE_RESTRICTION_ENABLED &&
229247 request . user . role !== Role . ADMIN
@@ -483,4 +501,93 @@ export class GamesController {
483501
484502 return query ;
485503 }
504+
505+ private async resolveMetadataRelationNameFilterGameIds (
506+ query : PaginateQuery ,
507+ ) : Promise < number [ ] | undefined > {
508+ const activeFilters = Object . entries ( metadataRelationNameFilters ) . filter (
509+ ( [ filterKey ] ) => query . filter ?. [ filterKey ] != null ,
510+ ) as Array <
511+ [
512+ MetadataRelationNameFilterKey ,
513+ ( typeof metadataRelationNameFilters ) [ MetadataRelationNameFilterKey ] ,
514+ ]
515+ > ;
516+
517+ if ( activeFilters . length === 0 ) {
518+ return undefined ;
519+ }
520+
521+ let matchingIds : number [ ] | undefined ;
522+
523+ for ( const [ queryFilterKey , metadataFilterKey ] of activeFilters ) {
524+ const filterValue = query . filter ?. [ queryFilterKey ] ;
525+
526+ if ( filterValue == null ) {
527+ continue ;
528+ }
529+
530+ const metadataQuery = {
531+ filter : {
532+ [ metadataFilterKey ] : filterValue ,
533+ } ,
534+ } as PaginateQuery ;
535+
536+ const queryBuilder = this . gamesRepository . manager
537+ . getRepository ( GameMetadata )
538+ . createQueryBuilder ( "metadata" )
539+ . select ( "game.id" , "id" )
540+ . distinct ( true )
541+ . innerJoin ( GamevaultGame , "game" , "game.metadata_id = metadata.id" ) ;
542+
543+ addFilter ( queryBuilder , metadataQuery , {
544+ [ metadataFilterKey ] : true ,
545+ } ) ;
546+
547+ const resolvedIds = (
548+ await queryBuilder . getRawMany < { id : number | string } > ( )
549+ )
550+ . map ( ( { id } ) => Number ( id ) )
551+ . filter ( ( id ) => Number . isInteger ( id ) ) ;
552+
553+ if ( matchingIds == null ) {
554+ matchingIds = resolvedIds ;
555+ } else {
556+ const resolvedIdSet = new Set ( resolvedIds ) ;
557+ matchingIds = matchingIds . filter ( ( id ) => resolvedIdSet . has ( id ) ) ;
558+ }
559+
560+ if ( matchingIds . length === 0 ) {
561+ break ;
562+ }
563+ }
564+
565+ return matchingIds ;
566+ }
567+
568+ private removeMetadataRelationNameFilters ( query : PaginateQuery ) {
569+ for ( const filterKey of Object . keys ( metadataRelationNameFilters ) ) {
570+ delete query . filter ?. [ filterKey ] ;
571+ }
572+ }
573+
574+ private applyResolvedGameIdFilter ( query : PaginateQuery , gameIds : number [ ] ) {
575+ query . filter ??= { } ;
576+
577+ const resolvedIdFilter =
578+ gameIds . length > 0 ? `$in:${ gameIds . join ( "," ) } ` : "$eq:-1" ;
579+ const existingIdFilter = query . filter . id ;
580+
581+ if ( existingIdFilter == null ) {
582+ query . filter . id = resolvedIdFilter ;
583+ return ;
584+ }
585+
586+ if ( isArray ( existingIdFilter ) ) {
587+ query . filter . id = [ ...existingIdFilter , resolvedIdFilter ] ;
588+ return ;
589+ }
590+
591+ query . filter . id = [ existingIdFilter , resolvedIdFilter ] ;
592+ }
486593}
0 commit comments