Skip to content

Commit bec7061

Browse files
committed
feat: refactor stats API to use schema validation and improve query handling
1 parent b3f234a commit bec7061

File tree

7 files changed

+68
-50
lines changed

7 files changed

+68
-50
lines changed
Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
1-
import { defineEventHandler, getQuery } from 'h3'
1+
import { defineEventHandler } from 'h3'
22
import { Monitor, StatisticsService } from '@tee/backend-ddd'
3-
import { StatsPeriodicity } from '@tee/common'
3+
import { statQueryParamsSchema } from '@tee/backend-ddd'
44

55
export default defineEventHandler(async (event) => {
66
try {
7-
const query = getQuery(event)
7+
const statsQuery = await getValidatedQuery(event, statQueryParamsSchema.parse)
88

9-
const periodicity = ['day', 'week', 'month', 'year'].includes(query.periodicity as StatsPeriodicity)
10-
? (query.periodicity as StatsPeriodicity)
11-
: undefined
12-
const since = typeof query.since === 'string' ? query.since : undefined
13-
const to = typeof query.to === 'string' ? query.to : undefined
14-
15-
const statsResult = await new StatisticsService().getNorthStarStats({ periodicity, since, to })
9+
const statsResult = await new StatisticsService().getNorthStarStats(statsQuery)
1610

1711
return statsResult
1812
} catch (error: any) {
1913
Monitor.error('Error in /api/stats', { error })
2014
throw createError({
2115
statusCode: 500,
22-
statusMessage: `Server internal error: ${error.message}`
16+
statusMessage: `Server internal error`,
17+
message: error.message
2318
})
2419
}
2520
})

libs/backend-ddd/src/statistics/application/statisticsService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { StatOutput, StatQueryParams } from '@tee/common'
21
import StatisticsFeatures from '../domain/statisticsFeatures'
2+
import { StatOutput, StatQueryParams } from '../domain/types'
33

44
export class StatisticsService {
55
public async getNorthStarStats(params: StatQueryParams): Promise<StatOutput> {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { z } from 'zod'
2+
import { StatsPeriodicity } from './types'
3+
4+
const statsPeriodicitySchema = z.nativeEnum(StatsPeriodicity)
5+
6+
export const statQueryParamsSchema = z.object({
7+
periodicity: statsPeriodicitySchema.optional(),
8+
since: z.coerce.date().optional(),
9+
to: z.coerce.date().optional()
10+
})

libs/backend-ddd/src/statistics/domain/statisticsFeatures.ts

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,73 @@
1-
import { Stat, StatOutput, StatQueryParams } from '@tee/common'
1+
import { Stat, StatOutput, StatQueryParams, StatsPeriodicity } from './types'
22

33
export default class StatisticsFeatures {
44
async fetchNorthStarStats(params: StatQueryParams): Promise<StatOutput> {
55
const METABASE_URL = 'http://tee-metabase.osc-fr1.scalingo.io/public/question/6969b2ab-ec49-44a0-9db8-3e2f9afbcf29.json'
66

7-
const periodicity = params.periodicity || 'month'
8-
9-
const today = new Date()
10-
const since = params.since ? new Date(params.since) : new Date(today.getFullYear(), 0, 1)
11-
const to = params.to ? new Date(params.to) : new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
12-
137
const response = await fetch(METABASE_URL)
148
if (!response.ok) {
159
throw new Error('Stats API: Failed to fetch Metabase data')
1610
}
1711

18-
const rawData: { week_start_date: string; total_2_3: number }[] = await response.json()
19-
const filtered = rawData
20-
.map((row) => ({
21-
date: this.toLocalDateOnly(new Date(row.week_start_date)), // week_start_date is a metabase variable that is poorly named and that is simply the statistic date
22-
value: row.total_2_3
12+
const data: { week_start_date: string; total_2_3: number }[] = await response.json()
13+
if (data.length === 0) {
14+
return {
15+
description: `Entreprises bénéficiaires`,
16+
stats: []
17+
}
18+
}
19+
20+
const periodicity = params.periodicity || StatsPeriodicity.Month
21+
const today = new Date()
22+
const since = params.since ?? new Date(today.getFullYear(), 0, 1)
23+
const to = params.to ?? new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
24+
25+
const filtered = data
26+
.map((datum) => ({
27+
date: new Date(datum.week_start_date), // week_start_date is a metabase variable that is poorly named and that is simply the statistic date
28+
value: datum.total_2_3
2329
}))
24-
.filter((row) => row.date >= since && row.date <= to)
30+
.filter((datum) => {
31+
return datum.date >= since && datum.date <= to
32+
})
2533

26-
const grouped: Record<string, { date: Date; value: number }> = {}
34+
const statsByPeriodicity: Record<string, Stat> = {}
2735

2836
filtered.forEach(({ date, value }) => {
29-
const d = new Date(date)
30-
if (d < since || d > to) return
31-
3237
let keyStatDate = today
3338

3439
switch (periodicity) {
35-
case 'day':
36-
keyStatDate = new Date(d.getFullYear(), d.getMonth(), d.getDate())
40+
case StatsPeriodicity.Day:
41+
// Set to the start of the day
42+
keyStatDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0))
3743
break
38-
case 'week': {
39-
const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - ((d.getDay() + 6) % 7))
40-
keyStatDate = weekStart
44+
case StatsPeriodicity.Week: {
45+
// Set to the start of the week (Monday)
46+
keyStatDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() - ((date.getDay() + 6) % 7), 0, 0, 0, 0))
4147
break
4248
}
43-
case 'month':
44-
keyStatDate = new Date(d.getFullYear(), d.getMonth(), 1)
49+
case StatsPeriodicity.Month:
50+
// Set to the start of the month
51+
keyStatDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0))
4552
break
46-
case 'year':
47-
keyStatDate = new Date(d.getFullYear(), 0, 1)
53+
case StatsPeriodicity.Year:
54+
// Set to the start of the year
55+
keyStatDate = new Date(Date.UTC(date.getFullYear(), 0, 1, 0, 0, 0, 0))
4856
break
4957
}
5058

51-
keyStatDate.setHours(keyStatDate.getHours() + 15) // To compensate for UTC hours and locales that can cause issues
52-
const key = keyStatDate.toISOString().slice(0, 10)
53-
if (!grouped[key]) {
54-
grouped[key] = { date: keyStatDate, value: 0 }
59+
const key = keyStatDate.toISOString()
60+
if (!statsByPeriodicity[key]) {
61+
statsByPeriodicity[key] = { date: keyStatDate.toISOString(), value: 0 }
5562
}
56-
grouped[key].value += value
63+
statsByPeriodicity[key].value += value
5764
})
5865

59-
const stats: Stat[] = Object.values(grouped)
60-
.sort((a, b) => a.date.getTime() - b.date.getTime())
66+
const stats: Stat[] = Object.values(statsByPeriodicity)
67+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
6168
.map((stat) => ({
6269
...stat,
63-
date: stat.date.toISOString().split('T')[0]
70+
date: stat.date.split('T')[0]
6471
}))
6572

6673
return {

libs/common/src/stats/types.ts renamed to libs/backend-ddd/src/statistics/domain/types.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
export type StatsPeriodicity = 'day' | 'week' | 'month' | 'year'
1+
export enum StatsPeriodicity {
2+
Day = 'day',
3+
Week = 'week',
4+
Month = 'month',
5+
Year = 'year'
6+
}
27

38
export interface StatQueryParams {
49
periodicity?: StatsPeriodicity
5-
since?: string
6-
to?: string
10+
since?: Date
11+
to?: Date
712
}
813

914
export interface Stat {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './application/statisticsService'
2+
export * from './domain/schemaValidator'

libs/common/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ export * from './establishment'
44
export * from './geoSearch/types'
55
export * from './opportunity'
66
export * from './questionnaire'
7-
export * from './stats/types'
7+
export * from '../../backend-ddd/src/statistics/domain/types'
88
export * from './validator'

0 commit comments

Comments
 (0)