feat: POST /corrections/eo — Gemini transcription + DeepSeek EO — 84/84 tests
This commit is contained in:
parent
77d5a8373e
commit
f4f8c55ce7
7 changed files with 422 additions and 2 deletions
|
|
@ -118,6 +118,11 @@ Si pendant l'implémentation Claude Code réalise que le plan doit être modifi
|
||||||
il STOP, signale le changement, explique pourquoi, et attend une nouvelle validation.
|
il STOP, signale le changement, explique pourquoi, et attend une nouvelle validation.
|
||||||
Il ne prend jamais de décision architecturale de sa propre initiative.
|
Il ne prend jamais de décision architecturale de sa propre initiative.
|
||||||
|
|
||||||
|
### Règle I — Pas de worktree Git
|
||||||
|
Claude Code ne crée jamais de worktree Git (`git worktree add`).
|
||||||
|
Toutes les modifications se font directement dans le dossier
|
||||||
|
du projet principal.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Structure du code — conventions
|
## 3. Structure du code — conventions
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { supabase } from '../lib/supabase'
|
import { supabase } from '../lib/supabase'
|
||||||
import { correctEE as deepseekCorrectEE } from '../lib/deepseek'
|
import { correctEE as deepseekCorrectEE, correctEO as deepseekCorrectEO } from '../lib/deepseek'
|
||||||
import type { EERapport } from '../lib/deepseek'
|
import type { EERapport, EORapport } from '../lib/deepseek'
|
||||||
import type { AuthProfile } from '../middleware/auth'
|
import type { AuthProfile } from '../middleware/auth'
|
||||||
|
|
||||||
type CorrectionError = {
|
type CorrectionError = {
|
||||||
|
|
@ -76,3 +76,71 @@ export async function correctEE(
|
||||||
// 4. Retourner le rapport complet
|
// 4. Retourner le rapport complet
|
||||||
return { data: rapport }
|
return { data: rapport }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function correctEO(
|
||||||
|
simulationId: string,
|
||||||
|
transcript: string,
|
||||||
|
tache: string,
|
||||||
|
profile: AuthProfile
|
||||||
|
): Promise<{ data: EORapport } | CorrectionError> {
|
||||||
|
// 1. Vérifier que la production existe et appartient à l'utilisateur
|
||||||
|
const { data: production, error: fetchError } = await supabase
|
||||||
|
.from('productions')
|
||||||
|
.select('id, user_id, tache')
|
||||||
|
.eq('id', simulationId)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (fetchError || !production) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'SIMULATION_NOT_FOUND',
|
||||||
|
message: 'Simulation introuvable.',
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (production.user_id !== profile.id) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'AUTH_REQUIRED',
|
||||||
|
message: 'Cette simulation ne vous appartient pas.',
|
||||||
|
status: 401,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Appeler DeepSeek pour la correction EO
|
||||||
|
let rapport: EORapport
|
||||||
|
try {
|
||||||
|
rapport = await deepseekCorrectEO(transcript, tache)
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: 'Erreur lors de la correction. Veuillez réessayer dans quelques instants.',
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mettre à jour la production dans Supabase
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('productions')
|
||||||
|
.update({
|
||||||
|
contenu: transcript,
|
||||||
|
score: rapport.score,
|
||||||
|
nclc: rapport.nclc,
|
||||||
|
rapport: JSON.stringify(rapport),
|
||||||
|
})
|
||||||
|
.eq('id', simulationId)
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: 'Erreur lors de la sauvegarde du rapport. Veuillez réessayer.',
|
||||||
|
status: 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Retourner le rapport complet
|
||||||
|
return { data: rapport }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,3 +128,100 @@ describe('deepseek.correctEE', () => {
|
||||||
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow()
|
await expect(correctEE('Texte', 'EE_T1')).rejects.toThrow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const VALID_RAPPORT_EO = {
|
||||||
|
score: 12,
|
||||||
|
nclc: 7,
|
||||||
|
criteres: [
|
||||||
|
{ nom: 'Coherence et cohesion', score: 4, commentaire: 'Discours structure.' },
|
||||||
|
{ nom: 'Lexique', score: 4, commentaire: 'Vocabulaire varie.' },
|
||||||
|
{ nom: 'Morphosyntaxe', score: 4, commentaire: 'Syntaxe correcte.' },
|
||||||
|
{ nom: 'Phonologie', score: 0, commentaire: 'Non evalue sur transcription textuelle.' },
|
||||||
|
],
|
||||||
|
erreurs: ['Hesitations frequentes', 'Registre parfois familier'],
|
||||||
|
production_modele: 'Transcription corrigee ici.',
|
||||||
|
suggestions_idees: ['Structurer les reponses', 'Enrichir le vocabulaire'],
|
||||||
|
exercices: ['Exercice fluidite orale', 'Exercice registre formel'],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('deepseek.correctEO', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retourne un rapport EO avec la bonne structure', async () => {
|
||||||
|
mockFetchSuccess(VALID_RAPPORT_EO)
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
const rapport = await correctEO('Ma transcription orale', 'EO_T1')
|
||||||
|
|
||||||
|
expect(rapport).toHaveProperty('score')
|
||||||
|
expect(rapport).toHaveProperty('nclc')
|
||||||
|
expect(rapport).toHaveProperty('criteres')
|
||||||
|
expect(rapport.criteres).toHaveLength(4)
|
||||||
|
expect(rapport).toHaveProperty('erreurs')
|
||||||
|
expect(rapport).toHaveProperty('production_modele')
|
||||||
|
expect(rapport).toHaveProperty('suggestions_idees')
|
||||||
|
expect(rapport).toHaveProperty('exercices')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('phonologie est a 0', async () => {
|
||||||
|
mockFetchSuccess(VALID_RAPPORT_EO)
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
const rapport = await correctEO('Ma transcription', 'EO_T1')
|
||||||
|
|
||||||
|
const phonologie = rapport.criteres.find((c) => c.nom === 'Phonologie')
|
||||||
|
expect(phonologie).toBeDefined()
|
||||||
|
expect(phonologie!.score).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('score est entre 0 et 20', async () => {
|
||||||
|
mockFetchSuccess(VALID_RAPPORT_EO)
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
const rapport = await correctEO('Ma transcription', 'EO_T3')
|
||||||
|
|
||||||
|
expect(rapport.score).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(rapport.score).toBeLessThanOrEqual(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('nclc est entre 4 et 12', async () => {
|
||||||
|
mockFetchSuccess(VALID_RAPPORT_EO)
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
const rapport = await correctEO('Ma transcription', 'EO_T1')
|
||||||
|
|
||||||
|
expect(rapport.nclc).toBeGreaterThanOrEqual(4)
|
||||||
|
expect(rapport.nclc).toBeLessThanOrEqual(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lance une erreur si score hors bornes', async () => {
|
||||||
|
mockFetchSuccess({ ...VALID_RAPPORT_EO, score: 25 })
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
await expect(correctEO('Transcription', 'EO_T1')).rejects.toThrow('Score invalide')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lance une erreur si nclc hors bornes', async () => {
|
||||||
|
mockFetchSuccess({ ...VALID_RAPPORT_EO, nclc: 2 })
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
await expect(correctEO('Transcription', 'EO_T1')).rejects.toThrow('NCLC invalide')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erreur HTTP depuis DeepSeek API', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const { correctEO } = await import('../deepseek')
|
||||||
|
|
||||||
|
await expect(correctEO('Transcription', 'EO_T1')).rejects.toThrow('DeepSeek API error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
71
src/lib/__tests__/gemini.test.ts
Normal file
71
src/lib/__tests__/gemini.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
function mockFetchSuccess(text: string) {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
candidates: [{ content: { parts: [{ text }] } }],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('gemini.transcribeAudio', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retourne une transcription non vide sur succes', async () => {
|
||||||
|
mockFetchSuccess('Bonjour, je suis candidat au TCF Canada.')
|
||||||
|
const { transcribeAudio } = await import('../gemini')
|
||||||
|
|
||||||
|
const result = await transcribeAudio('base64audio', 'audio/webm')
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string')
|
||||||
|
expect(result.length).toBeGreaterThan(0)
|
||||||
|
expect(result).toBe('Bonjour, je suis candidat au TCF Canada.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erreur HTTP depuis Gemini API', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const { transcribeAudio } = await import('../gemini')
|
||||||
|
|
||||||
|
await expect(transcribeAudio('base64audio', 'audio/webm')).rejects.toThrow(
|
||||||
|
'Gemini API error'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erreur si transcription vide', async () => {
|
||||||
|
mockFetchSuccess('')
|
||||||
|
const { transcribeAudio } = await import('../gemini')
|
||||||
|
|
||||||
|
await expect(transcribeAudio('base64audio', 'audio/webm')).rejects.toThrow(
|
||||||
|
'Gemini API: transcription vide'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('erreur si reponse sans candidates', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ candidates: [] }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const { transcribeAudio } = await import('../gemini')
|
||||||
|
|
||||||
|
await expect(transcribeAudio('base64audio', 'audio/webm')).rejects.toThrow(
|
||||||
|
'Gemini API: transcription vide'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -17,6 +17,22 @@ export interface EERapport {
|
||||||
exercices: string[]
|
exercices: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EOCritere {
|
||||||
|
nom: string
|
||||||
|
score: number
|
||||||
|
commentaire: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EORapport {
|
||||||
|
score: number
|
||||||
|
nclc: number
|
||||||
|
criteres: EOCritere[]
|
||||||
|
erreurs: string[]
|
||||||
|
production_modele: string
|
||||||
|
suggestions_idees: string[]
|
||||||
|
exercices: string[]
|
||||||
|
}
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `Tu es un examinateur officiel du TCF Canada (Test de connaissance du français).
|
const SYSTEM_PROMPT = `Tu es un examinateur officiel du TCF Canada (Test de connaissance du français).
|
||||||
Tu évalues une production écrite selon les 4 critères officiels de l'Expression Écrite :
|
Tu évalues une production écrite selon les 4 critères officiels de l'Expression Écrite :
|
||||||
1. Cohérence et cohésion
|
1. Cohérence et cohésion
|
||||||
|
|
@ -89,3 +105,76 @@ export async function correctEE(contenu: string, tache: string): Promise<EERappo
|
||||||
|
|
||||||
return rapport
|
return rapport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT_EO = `Tu es un examinateur officiel du TCF Canada (Test de connaissance du français).
|
||||||
|
Tu évalues une production orale à partir de sa transcription selon les 4 critères officiels de l'Expression Orale :
|
||||||
|
1. Cohérence et cohésion
|
||||||
|
2. Lexique (étendue et maîtrise du vocabulaire)
|
||||||
|
3. Morphosyntaxe (grammaire et structures)
|
||||||
|
4. Phonologie — NOTE IMPORTANTE : ce critère est fixé à 0 car l'évaluation se fait sur une transcription textuelle, pas sur l'audio original. Mets toujours 0 pour ce critère.
|
||||||
|
|
||||||
|
Tu dois retourner un JSON strict avec cette structure exacte :
|
||||||
|
{
|
||||||
|
"score": <number 0-20>,
|
||||||
|
"nclc": <number 4-12>,
|
||||||
|
"criteres": [
|
||||||
|
{ "nom": "Cohérence et cohésion", "score": <number 0-5>, "commentaire": "<string>" },
|
||||||
|
{ "nom": "Lexique", "score": <number 0-5>, "commentaire": "<string>" },
|
||||||
|
{ "nom": "Morphosyntaxe", "score": <number 0-5>, "commentaire": "<string>" },
|
||||||
|
{ "nom": "Phonologie", "score": 0, "commentaire": "Non évalué sur transcription textuelle." }
|
||||||
|
],
|
||||||
|
"erreurs": ["<erreur 1>", "<erreur 2>", ...],
|
||||||
|
"production_modele": "<version corrigée de la transcription>",
|
||||||
|
"suggestions_idees": ["<idée 1>", "<idée 2>", ...],
|
||||||
|
"exercices": ["<exercice recommandé 1>", "<exercice recommandé 2>", ...]
|
||||||
|
}
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- score est la note globale sur 20 (basée uniquement sur les 3 critères évalués)
|
||||||
|
- nclc est le niveau NCLC estimé (entre 4 et 12)
|
||||||
|
- Phonologie est toujours à 0 avec le commentaire "Non évalué sur transcription textuelle."
|
||||||
|
- Retourne UNIQUEMENT le JSON, sans texte avant ni après`
|
||||||
|
|
||||||
|
export async function correctEO(transcript: string, tache: string): Promise<EORapport> {
|
||||||
|
const response = await fetch(`${DEEPSEEK_BASE_URL}/chat/completions`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${DEEPSEEK_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: SYSTEM_PROMPT_EO },
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `Tâche : ${tache}\n\nTranscription de la production orale :\n${transcript}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0.3,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`DeepSeek API error: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
const content = data.choices?.[0]?.message?.content
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('DeepSeek API: réponse vide')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rapport: EORapport = JSON.parse(content)
|
||||||
|
|
||||||
|
if (rapport.score < 0 || rapport.score > 20) {
|
||||||
|
throw new Error(`Score invalide: ${rapport.score} (attendu 0-20)`)
|
||||||
|
}
|
||||||
|
if (rapport.nclc < 4 || rapport.nclc > 12) {
|
||||||
|
throw new Error(`NCLC invalide: ${rapport.nclc} (attendu 4-12)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rapport
|
||||||
|
}
|
||||||
|
|
|
||||||
38
src/lib/gemini.ts
Normal file
38
src/lib/gemini.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
const GEMINI_API_KEY = process.env.GEMINI_API_KEY ?? ''
|
||||||
|
const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'
|
||||||
|
|
||||||
|
export async function transcribeAudio(
|
||||||
|
audioBase64: string,
|
||||||
|
mimeType: string
|
||||||
|
): Promise<string> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${GEMINI_BASE_URL}/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
parts: [
|
||||||
|
{ inlineData: { mimeType, data: audioBase64 } },
|
||||||
|
{ text: 'Transcris cet audio mot pour mot en francais. Retourne uniquement la transcription, sans commentaire.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Gemini API error: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
const text = data.candidates?.[0]?.content?.parts?.[0]?.text
|
||||||
|
|
||||||
|
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
||||||
|
throw new Error('Gemini API: transcription vide')
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.trim()
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import type { AppVariables } from '../middleware/auth'
|
||||||
import * as correctionController from '../controllers/correctionController'
|
import * as correctionController from '../controllers/correctionController'
|
||||||
|
|
||||||
const VALID_TACHES_EE = ['EE_T1', 'EE_T2', 'EE_T3']
|
const VALID_TACHES_EE = ['EE_T1', 'EE_T2', 'EE_T3']
|
||||||
|
const VALID_TACHES_EO = ['EO_T1', 'EO_T3']
|
||||||
|
|
||||||
const corrections = new Hono<{ Variables: AppVariables }>()
|
const corrections = new Hono<{ Variables: AppVariables }>()
|
||||||
|
|
||||||
|
|
@ -58,4 +59,55 @@ corrections.post('/ee', authMiddleware, async (c) => {
|
||||||
return c.json(result.data, 200)
|
return c.json(result.data, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
corrections.post('/eo', authMiddleware, async (c) => {
|
||||||
|
let body: { simulationId?: unknown; transcript?: unknown; tache?: unknown }
|
||||||
|
try {
|
||||||
|
body = await c.req.json()
|
||||||
|
} catch {
|
||||||
|
return c.json(
|
||||||
|
{ error: true, code: 'VALIDATION_ERROR', message: 'Corps de la requête invalide.' },
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.simulationId || typeof body.simulationId !== 'string') {
|
||||||
|
return c.json(
|
||||||
|
{ error: true, code: 'VALIDATION_ERROR', message: 'simulationId est requis.' },
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.transcript || typeof body.transcript !== 'string') {
|
||||||
|
return c.json(
|
||||||
|
{ error: true, code: 'VALIDATION_ERROR', message: 'transcript est requis.' },
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.tache || !VALID_TACHES_EO.includes(body.tache as string)) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
code: 'VALIDATION_ERROR',
|
||||||
|
message: `Tâche invalide. Valeurs acceptées : ${VALID_TACHES_EO.join(', ')}`,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = c.get('profile')
|
||||||
|
const result = await correctionController.correctEO(
|
||||||
|
body.simulationId as string,
|
||||||
|
body.transcript as string,
|
||||||
|
body.tache as string,
|
||||||
|
profile
|
||||||
|
)
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
return c.json(result, result.status as 401 | 404 | 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(result.data, 200)
|
||||||
|
})
|
||||||
|
|
||||||
export default corrections
|
export default corrections
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue