Skip to content

Commit

Permalink
Merge pull request #320 from incubateur-ademe/feat/stats-api-endpoint
Browse files Browse the repository at this point in the history
Feat/stats api endpoint
  • Loading branch information
mehdilouraoui authored Jan 14, 2025
2 parents fff519f + b60cafe commit 968779d
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@sentry/nextjs": "7.120.1",
"@splidejs/react-splide": "^0.7.12",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"html-to-image": "^1.11.11",
"jspdf": "^2.5.2",
"leaflet": "^1.9.4",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from "next/server";
import z from "zod";
import { add } from "date-fns/add";
import { startOfYear } from "date-fns/startOfYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfWeek } from "date-fns/startOfWeek";
import { startOfDay } from "date-fns/startOfDay";
import { getNorthStarStats } from "@/src/lib/prisma/prisma-analytics-queries";
import { fr } from "date-fns/locale/fr";

const StatsRouteSchema = z.object({
since: z
.number()
.positive()
.max(5000, { message: "Veuillez rentrer une valeur inférieure à 5000 pour le paramètre since" }),
periodicity: z.enum(["year", "month", "week", "day"]),
});

interface StatOutputRecord {
value: number;
date: Date;
}

type StatOutput = {
description?: string;
stats: StatOutputRecord[];
};

export async function GET(request: NextRequest) {
const parsedRequest = StatsRouteSchema.safeParse({
since: +(request.nextUrl.searchParams.get("since") ?? 0),
periodicity: request.nextUrl.searchParams.get("periodicity"),
});
if (!parsedRequest.success) {
const { errors } = parsedRequest.error;
return NextResponse.json({ error: { message: "Invalid request", errors } }, { status: 400 });
} else {
const { since: nbIntervals, periodicity } = parsedRequest.data;

const ranges = {
year: startOfYear,
month: startOfMonth,
week: (date: Date) => startOfWeek(date, { weekStartsOn: 1, locale: fr }),
day: startOfDay,
};

let dateBeginOfLastPeriod = new Date();
dateBeginOfLastPeriod = ranges[periodicity](new Date());
dateBeginOfLastPeriod = add(dateBeginOfLastPeriod, {
minutes: -dateBeginOfLastPeriod.getTimezoneOffset(),
});

const dateBeginOfFirstPeriod = add(dateBeginOfLastPeriod, {
...(periodicity === "year" && { years: 1 - nbIntervals }),
...(periodicity === "month" && { months: 1 - nbIntervals }),
...(periodicity === "week" && { weeks: 1 - nbIntervals }),
...(periodicity === "day" && { days: 1 - nbIntervals }),
});

const results = await getNorthStarStats({ dateFrom: dateBeginOfFirstPeriod, range: periodicity });

const sanitizeResults: StatOutput = {
stats: results.map((result) => ({
value: Number(result.score),
date: result.periode!,
})),
};

return NextResponse.json(sanitizeResults);
}
}
2 changes: 2 additions & 0 deletions src/helpers/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type DateRange = "day" | "week" | "month" | "year";

export const FAR_FUTURE = new Date(3024, 0, 0, 1);

export const removeDaysToDate = (date: Date, nbDays: number) => new Date(date.getTime() - nbDays * 24 * 60 * 60 * 1000);
Expand Down
41 changes: 40 additions & 1 deletion src/lib/prisma/prisma-analytics-queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Analytics } from "@prisma/client";
/* eslint-disable max-len */

import { Analytics, Prisma } from "@prisma/client";
import { prismaClient } from "./prismaClient";
import { DateRange } from "@/src/helpers/dateUtils";

type AnalyticsProps = Omit<Analytics, "id" | "created_at" | "created_by">;

Expand All @@ -14,3 +17,39 @@ export const createAnalytic = async (analytics: AnalyticsProps): Promise<Analyti
},
});
};

type GetNorthStarStatsProps = {
dateFrom: Date;
range: DateRange;
};

export const getNorthStarStats = async (
params: GetNorthStarStatsProps,
): Promise<{ periode: Date; score: number }[]> => {
return prismaClient.$queryRaw(
Prisma.sql`with score_table as (
select
date_trunc(${params.range}, p.created_at) as date1,
count(distinct p.id) as score
from pfmv."projet" p
join pfmv."user_projet" up on p.id = up.projet_id
join pfmv."User" u on up.user_id = u.id
WHERE p.created_at >= ${params.dateFrom}
and p.deleted_at IS NULL
and u.email NOT LIKE '%@ademe.fr'
and u.email NOT LIKE '%@beta.gouv.fr'
and u.email != '[email protected]'
group by 1
),
all_intervals as (
SELECT date_trunc(${params.range}, ts) as date1
FROM generate_series(${params.dateFrom}, now(), CONCAT('1 ', ${params.range})::interval) AS ts
)
select
all_intervals.date1::timestamp::date as "periode",
coalesce(score, 0) as "score"
from score_table
right outer join all_intervals on all_intervals.date1 = score_table.date1
order by 1;`,
);
};

0 comments on commit 968779d

Please sign in to comment.