Compare commits

..

2 Commits

Author SHA1 Message Date
ebd8573299 perf: suppression des $queryRaw redondants et cache sur getTemplates
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 3s
- getEvaluation/getTemplates: retire les $queryRaw qui dupliquaient les
  données déjà chargées via Prisma include (2 requêtes DB → 1)
- getTemplates: wrappé avec cache() React pour dédupliquer les appels
  dans le même render tree
- Supprime l'import Prisma devenu inutile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:31:08 +01:00
27866091bf perf: optimisations DB — batch queries et index
- createEvaluation: remplace N create() par un createMany() (N→1 requête)
- updateEvaluation: regroupe les upserts en $transaction() parallèle
- Ajout d'index sur Evaluation.evaluatorId, Evaluation.templateId,
  EvaluationShare.userId et AuditLog.evaluationId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 13:27:57 +01:00
4 changed files with 54 additions and 73 deletions

View File

@@ -0,0 +1,11 @@
-- CreateIndex
CREATE INDEX "AuditLog_evaluationId_idx" ON "AuditLog"("evaluationId");
-- CreateIndex
CREATE INDEX "Evaluation_evaluatorId_idx" ON "Evaluation"("evaluatorId");
-- CreateIndex
CREATE INDEX "Evaluation_templateId_idx" ON "Evaluation"("templateId");
-- CreateIndex
CREATE INDEX "EvaluationShare_userId_idx" ON "EvaluationShare"("userId");

View File

@@ -66,6 +66,9 @@ model Evaluation {
isPublic Boolean @default(false) // visible par tous (ex. démo)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([evaluatorId])
@@index([templateId])
}
model EvaluationShare {
@@ -77,6 +80,7 @@ model EvaluationShare {
createdAt DateTime @default(now())
@@unique([evaluationId, userId])
@@index([userId])
}
model DimensionScore {
@@ -106,4 +110,6 @@ model AuditLog {
newValue String?
userId String?
createdAt DateTime @default(now())
@@index([evaluationId])
}

View File

@@ -72,11 +72,12 @@ export async function createEvaluation(data: {
},
});
for (const dim of template.dimensions) {
await prisma.dimensionScore.create({
data: { evaluationId: evaluation.id, dimensionId: dim.id },
});
}
await prisma.dimensionScore.createMany({
data: template.dimensions.map((dim) => ({
evaluationId: evaluation.id,
dimensionId: dim.id,
})),
});
revalidatePath("/dashboard");
return { success: true, data: { id: evaluation.id } };
@@ -177,30 +178,33 @@ export async function updateEvaluation(id: string, data: UpdateEvaluationInput):
}
if (dimensionScores && Array.isArray(dimensionScores)) {
for (const ds of dimensionScores) {
if (ds.dimensionId) {
await prisma.dimensionScore.upsert({
where: {
evaluationId_dimensionId: { evaluationId: id, dimensionId: ds.dimensionId },
},
update: {
score: ds.score,
justification: ds.justification,
examplesObserved: ds.examplesObserved,
confidence: ds.confidence,
candidateNotes: ds.candidateNotes,
},
create: {
evaluationId: id,
dimensionId: ds.dimensionId,
score: ds.score,
justification: ds.justification,
examplesObserved: ds.examplesObserved,
confidence: ds.confidence,
candidateNotes: ds.candidateNotes,
},
});
}
const validScores = dimensionScores.filter((ds) => ds.dimensionId);
if (validScores.length > 0) {
await prisma.$transaction(
validScores.map((ds) =>
prisma.dimensionScore.upsert({
where: {
evaluationId_dimensionId: { evaluationId: id, dimensionId: ds.dimensionId },
},
update: {
score: ds.score,
justification: ds.justification,
examplesObserved: ds.examplesObserved,
confidence: ds.confidence,
candidateNotes: ds.candidateNotes,
},
create: {
evaluationId: id,
dimensionId: ds.dimensionId,
score: ds.score,
justification: ds.justification,
examplesObserved: ds.examplesObserved,
confidence: ds.confidence,
candidateNotes: ds.candidateNotes,
},
})
)
);
}
}

View File

@@ -1,4 +1,4 @@
import { Prisma } from "@prisma/client";
import { cache } from "react";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import { canAccessEvaluation } from "@/lib/evaluation-access";
@@ -62,60 +62,20 @@ export async function getEvaluation(id: string) {
);
if (!hasAccess) return null;
const templateId = evaluation.templateId;
const dimsRaw = evaluation.template
? ((await prisma.$queryRaw(
Prisma.sql`SELECT id, slug, title, rubric, "orderIndex", "suggestedQuestions" FROM "TemplateDimension" WHERE "templateId" = ${templateId} ORDER BY "orderIndex" ASC`
)) as { id: string; slug: string; title: string; rubric: string; orderIndex: number; suggestedQuestions: string | null }[])
: [];
const dimMap = new Map(dimsRaw.map((d) => [d.id, d]));
return {
...evaluation,
evaluationDate: evaluation.evaluationDate.toISOString(),
template: evaluation.template
? {
...evaluation.template,
dimensions: evaluation.template.dimensions.map((d) => {
const raw = dimMap.get(d.id);
return {
...d,
suggestedQuestions: raw?.suggestedQuestions ?? d.suggestedQuestions,
};
}),
}
: null,
dimensionScores: evaluation.dimensionScores.map((ds) => ({
...ds,
dimension: ds.dimension
? {
...ds.dimension,
suggestedQuestions: dimMap.get(ds.dimension.id)?.suggestedQuestions ?? ds.dimension.suggestedQuestions,
}
: null,
})),
};
}
export async function getTemplates() {
export const getTemplates = cache(async () => {
const templates = await prisma.template.findMany({
include: {
dimensions: { orderBy: { orderIndex: "asc" } },
},
});
const dimsRaw = (await prisma.$queryRaw(
Prisma.sql`SELECT id, "templateId", slug, title, rubric, "orderIndex", "suggestedQuestions" FROM "TemplateDimension" ORDER BY "templateId", "orderIndex"`
)) as { id: string; templateId: string; slug: string; title: string; rubric: string; orderIndex: number; suggestedQuestions: string | null }[];
const dimMap = new Map(dimsRaw.map((d) => [d.id, d]));
return templates.map((t) => ({
...t,
dimensions: t.dimensions.map((d) => ({
...d,
suggestedQuestions: dimMap.get(d.id)?.suggestedQuestions ?? d.suggestedQuestions,
})),
}));
}
return templates;
});
export async function getUsers() {
const session = await auth();