11import {
2+ Alliance ,
3+ BoolTy ,
24 DESCRIPTORS ,
35 DateTimeTy ,
46 EventTypeOption ,
57 IntTy ,
8+ RegionOption ,
9+ RemoteOption ,
610 Season ,
11+ TournamentLevel ,
712 getEventTypes ,
13+ getRegionCodes ,
814 list ,
915 nn ,
1016 nullTy ,
@@ -17,7 +23,58 @@ import { EventGQL } from "./Event";
1723import { Event } from "../../db/entities/Event" ;
1824import { MatchGQL } from "./Match" ;
1925import { MatchScore } from "../../db/entities/dyn/match-score" ;
20- import { EventTypeOptionGQL } from "./enums" ;
26+ import { EventTypeOptionGQL , RegionOptionGQL , RemoteOptionGQL } from "./enums" ;
27+ import { GraphQLObjectType } from "graphql" ;
28+ import { singleSeasonScoreAwareMatchLoader } from "./Match" ;
29+ import { DateTime } from "luxon" ;
30+ import { SelectQueryBuilder } from "typeorm" ;
31+
32+ const NoteworthyMatchesGQL = new GraphQLObjectType ( {
33+ name : "NoteworthyMatches" ,
34+ fields : {
35+ highScore : { type : list ( nn ( MatchGQL ) ) } ,
36+ combinedScore : { type : list ( nn ( MatchGQL ) ) } ,
37+ losingScore : { type : list ( nn ( MatchGQL ) ) } ,
38+ } ,
39+ } ) ;
40+
41+ type MatchKey = { eventSeason : Season ; eventCode : string ; id : number } ;
42+
43+ async function loadMatches ( keys : MatchKey [ ] ) {
44+ if ( ! keys . length ) return [ ] ;
45+ const matches = await singleSeasonScoreAwareMatchLoader ( keys , [ ] , true , true ) ;
46+ const byKey = new Map ( matches . map ( ( m ) => [ `${ m . eventSeason } -${ m . eventCode } -${ m . id } ` , m ] ) ) ;
47+ return keys
48+ . map ( ( k ) => byKey . get ( `${ k . eventSeason } -${ k . eventCode } -${ k . id } ` ) )
49+ . filter ( ( m ) : m is Match => ! ! m ) ;
50+ }
51+
52+ function applyEventFilters (
53+ q : SelectQueryBuilder < Match > ,
54+ {
55+ region,
56+ type,
57+ remote,
58+ } : {
59+ region ?: RegionOption | null | undefined ;
60+ type ?: EventTypeOption | null | undefined ;
61+ remote ?: RemoteOption | null | undefined ;
62+ }
63+ ) {
64+ if ( region && region !== RegionOption . All ) {
65+ q . andWhere ( "e.region_code IN (:...regions)" , { regions : getRegionCodes ( region ) } ) ;
66+ }
67+
68+ if ( type && type !== EventTypeOption . All ) {
69+ q . andWhere ( "e.type IN (:...types)" , { types : getEventTypes ( type ) } ) ;
70+ }
71+
72+ if ( remote && remote !== RemoteOption . All ) {
73+ q . andWhere ( "e.remote = :remote" , { remote : remote === RemoteOption . Remote } ) ;
74+ }
75+
76+ return q ;
77+ }
2178
2279export const HomeQueries : Record < string , GraphQLFieldConfig < any , any > > = {
2380 activeTeamsCount : {
@@ -119,4 +176,170 @@ export const HomeQueries: Record<string, GraphQLFieldConfig<any, any>> = {
119176 . getOne ( ) ;
120177 } ,
121178 } ,
179+
180+ upcomingMatches : {
181+ type : list ( nn ( MatchGQL ) ) ,
182+ args : {
183+ season : IntTy ,
184+ limit : nullTy ( IntTy ) ,
185+ minutes : nullTy ( IntTy ) ,
186+ region : { type : RegionOptionGQL } ,
187+ type : { type : EventTypeOptionGQL } ,
188+ remote : { type : RemoteOptionGQL } ,
189+ elim : nullTy ( BoolTy ) ,
190+ } ,
191+ resolve : async (
192+ _ ,
193+ {
194+ season,
195+ limit,
196+ minutes,
197+ region,
198+ type,
199+ remote,
200+ elim,
201+ } : {
202+ season : number ;
203+ limit ?: number | null ;
204+ minutes ?: number | null ;
205+ region ?: RegionOption | null ;
206+ type ?: EventTypeOption | null ;
207+ remote ?: RemoteOption | null ;
208+ elim ?: boolean | null ;
209+ }
210+ ) => {
211+ const windowMinutes = minutes ?? 60 * 24 ;
212+ const start = DateTime . now ( ) . minus ( { minutes : 5 } ) . toJSDate ( ) ;
213+ const end = DateTime . now ( ) . plus ( { minutes : windowMinutes } ) . toJSDate ( ) ;
214+
215+ let q = DATA_SOURCE . getRepository ( Match )
216+ . createQueryBuilder ( "m" )
217+ . leftJoin ( Event , "e" , "e.season = m.event_season AND e.code = m.event_code" )
218+ . select ( "m.event_season" , "eventSeason" )
219+ . addSelect ( "m.event_code" , "eventCode" )
220+ . addSelect ( "m.id" , "id" )
221+ . where ( "m.event_season = :season" , { season } )
222+ . andWhere ( "NOT m.has_been_played" )
223+ . andWhere ( "m.scheduled_start_time IS NOT NULL" )
224+ . andWhere ( "m.scheduled_start_time > :start" , { start } )
225+ . andWhere ( "m.scheduled_start_time < :end" , { end } ) ;
226+
227+ if ( elim !== null && elim !== undefined ) {
228+ if ( elim ) {
229+ q . andWhere ( "m.tournament_level <> :quals" , { quals : TournamentLevel . Quals } ) ;
230+ } else {
231+ q . andWhere ( "m.tournament_level = :quals" , { quals : TournamentLevel . Quals } ) ;
232+ }
233+ }
234+
235+ q = applyEventFilters ( q , { region, type, remote } ) ;
236+
237+ q . orderBy ( "m.scheduled_start_time" , "ASC" ) . limit ( Math . min ( limit ?? 50 , 100 ) ) ;
238+
239+ const raw = await q . getRawMany ( ) ;
240+ const keys : MatchKey [ ] = raw . map ( ( r ) => ( {
241+ eventSeason : + r . eventSeason as Season ,
242+ eventCode : r . eventCode ,
243+ id : + r . id ,
244+ } ) ) ;
245+
246+ return loadMatches ( keys ) ;
247+ } ,
248+ } ,
249+
250+ noteworthyMatches : {
251+ type : NoteworthyMatchesGQL ,
252+ args : {
253+ season : IntTy ,
254+ limit : nullTy ( IntTy ) ,
255+ region : { type : RegionOptionGQL } ,
256+ type : { type : EventTypeOptionGQL } ,
257+ remote : { type : RemoteOptionGQL } ,
258+ } ,
259+ resolve : async (
260+ _ ,
261+ {
262+ season,
263+ limit,
264+ region,
265+ type,
266+ remote,
267+ } : {
268+ season : number ;
269+ limit ?: number | null ;
270+ region ?: RegionOption | null ;
271+ type ?: EventTypeOption | null ;
272+ remote ?: RemoteOption | null ;
273+ }
274+ ) => {
275+ const descriptor = DESCRIPTORS [ season as Season ] ;
276+ const scoreKey = descriptor . pensSubtract ? "totalPoints" : "totalPointsNp" ;
277+ const ns = DATA_SOURCE . namingStrategy ;
278+ const scoreCol = ns . columnName ( scoreKey , undefined , [ ] ) ;
279+
280+ const base = DATA_SOURCE . getRepository ( Match )
281+ . createQueryBuilder ( "m" )
282+ . innerJoin (
283+ `match_score_${ season } ` ,
284+ "red" ,
285+ "m.event_season = red.season AND m.event_code = red.event_code AND m.id = red.match_id AND red.alliance = :red" ,
286+ { red : Alliance . Red }
287+ )
288+ . innerJoin (
289+ `match_score_${ season } ` ,
290+ "blue" ,
291+ "m.event_season = blue.season AND m.event_code = blue.event_code AND m.id = blue.match_id AND blue.alliance = :blue" ,
292+ { blue : Alliance . Blue }
293+ )
294+ . leftJoin ( Event , "e" , "e.season = m.event_season AND e.code = m.event_code" )
295+ . select ( "m.event_season" , "eventSeason" )
296+ . addSelect ( "m.event_code" , "eventCode" )
297+ . addSelect ( "m.id" , "id" )
298+ . where ( "m.event_season = :season" , { season } )
299+ . andWhere ( "m.has_been_played" )
300+ . andWhere ( "NOT e.modified_rules" ) ;
301+
302+ const filtered = applyEventFilters ( base , { region, type, remote } ) ;
303+
304+ const maxScoreExpr = `GREATEST(red.${ scoreCol } , blue.${ scoreCol } )` ;
305+ const sumScoreExpr = `(red.${ scoreCol } + blue.${ scoreCol } )` ;
306+ const losingScoreExpr = `LEAST(red.${ scoreCol } , blue.${ scoreCol } )` ;
307+
308+ const cap = Math . min ( limit ?? 30 , 100 ) ;
309+
310+ const highRaw = await filtered
311+ . clone ( )
312+ . orderBy ( maxScoreExpr , "DESC" )
313+ . addOrderBy ( "m.scheduled_start_time" , "ASC" )
314+ . limit ( cap )
315+ . getRawMany ( ) ;
316+
317+ const combinedRaw = await filtered
318+ . clone ( )
319+ . orderBy ( sumScoreExpr , "DESC" )
320+ . addOrderBy ( "m.scheduled_start_time" , "ASC" )
321+ . limit ( cap )
322+ . getRawMany ( ) ;
323+
324+ const losingRaw = await filtered
325+ . clone ( )
326+ . orderBy ( losingScoreExpr , "DESC" )
327+ . addOrderBy ( "m.scheduled_start_time" , "ASC" )
328+ . limit ( cap )
329+ . getRawMany ( ) ;
330+
331+ const toKeys = ( rows : any [ ] ) : MatchKey [ ] =>
332+ rows . map ( ( r ) => ( {
333+ eventSeason : + r . eventSeason as Season ,
334+ eventCode : r . eventCode ,
335+ id : + r . id ,
336+ } ) ) ;
337+
338+ return {
339+ highScore : await loadMatches ( toKeys ( highRaw ) ) ,
340+ combinedScore : await loadMatches ( toKeys ( combinedRaw ) ) ,
341+ losingScore : await loadMatches ( toKeys ( losingRaw ) ) ,
342+ } ;
343+ } ,
344+ } ,
122345} ;
0 commit comments