feat(simulations): persistance session — autosave + sujet_id + getById tolère rapport=null (FTD-21)

This commit is contained in:
Hermann_Kitio 2026-04-21 03:48:45 +03:00
parent fc76fac981
commit fcd8fe7017
3 changed files with 533 additions and 77 deletions

View file

@ -66,7 +66,7 @@ function mockProductionSelect(data: unknown, error: unknown = null) {
} as any)
}
/** Mock from('sujets').select(...).eq(...).eq(...).eq(...) pour create */
/** Mock from('sujets').select(...).eq(...).eq(...).eq(...) pour create (liste filtrée) */
function mockSujets(rows: unknown[]) {
vi.mocked(supabase.from).mockReturnValueOnce({
select: vi.fn(() => ({
@ -79,6 +79,26 @@ function mockSujets(rows: unknown[]) {
} as any)
}
/** Mock from('sujets').select(...).eq(...).single() pour getById/updateSujet */
function mockSujetById(data: unknown, error: unknown = null) {
vi.mocked(supabase.from).mockReturnValueOnce({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => ({ data, error })),
})),
})),
} as any)
}
/** Mock from('productions').update(...).eq(...) pour autosave/updateSujet */
function mockUpdate(error: unknown = null) {
vi.mocked(supabase.from).mockReturnValueOnce({
update: vi.fn(() => ({
eq: vi.fn(() => ({ error })),
})),
} as any)
}
const MOCK_SUJET_EE_T1 = {
id: 'sujet-1',
consigne: 'Écrivez un texte argumentatif sur le télétravail.',
@ -117,8 +137,8 @@ describe('POST /simulations', () => {
it('free + 4 simulations utilisées → création OK, pas d\'incrément (il a lieu après correction)', async () => {
const profile = buildProfile({ plan: 'free', simulations_used: 4 })
mockAuth(profile)
mockInsert({ id: 'prod-1', tache: 'EE_T1', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
mockSujets([MOCK_SUJET_EE_T1])
mockInsert({ id: 'prod-1', tache: 'EE_T1', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
const app = createApp()
const res = await app.request('/simulations', {
@ -133,9 +153,10 @@ describe('POST /simulations', () => {
expect(body.tache).toBe('EE_T1')
expect(body.mode).toBe('entrainement')
expect(body.sujet).toEqual(MOCK_SUJET_EE_T1)
// 3 appels from : profiles (auth) + productions (insert) + sujets (select)
// 3 appels from : profiles (auth) + sujets (select) + productions (insert)
expect(vi.mocked(supabase.from).mock.calls).toHaveLength(3)
expect(vi.mocked(supabase.from).mock.calls[2][0]).toBe('sujets')
expect(vi.mocked(supabase.from).mock.calls[1][0]).toBe('sujets')
expect(vi.mocked(supabase.from).mock.calls[2][0]).toBe('productions')
})
it('free + 5 simulations utilisées → QUOTA_REACHED', async () => {
@ -160,9 +181,8 @@ describe('POST /simulations', () => {
it('standard + 999 simulations → création OK, simulations_used NON incrémenté', async () => {
const profile = buildProfile({ plan: 'standard', simulations_used: 999 })
mockAuth(profile)
mockInsert({ id: 'prod-2', tache: 'EE_T2', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
mockSujets([MOCK_SUJET_EE_T2])
// Pas de mockUpdate : standard n'a pas de limite
mockInsert({ id: 'prod-2', tache: 'EE_T2', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
const app = createApp()
const res = await app.request('/simulations', {
@ -175,9 +195,10 @@ describe('POST /simulations', () => {
const body = await res.json()
expect(body.id).toBe('prod-2')
expect(body.sujet).toEqual(MOCK_SUJET_EE_T2)
// 3 appels from : profiles (auth) + productions (insert) + sujets (select) — pas de update
// 3 appels from : profiles (auth) + sujets (select) + productions (insert)
expect(vi.mocked(supabase.from).mock.calls).toHaveLength(3)
expect(vi.mocked(supabase.from).mock.calls[2][0]).toBe('sujets')
expect(vi.mocked(supabase.from).mock.calls[1][0]).toBe('sujets')
expect(vi.mocked(supabase.from).mock.calls[2][0]).toBe('productions')
})
it('premium + EO_T2_LIVE → création OK, sujet: null (pas de lookup)', async () => {
@ -185,7 +206,6 @@ describe('POST /simulations', () => {
mockAuth(profile)
mockInsert({ id: 'prod-3', tache: 'EO_T2_LIVE', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
// Pas de mockSujets : EO_T2_LIVE skip la query sujets
// Pas de mockUpdate : premium n'a pas de limite
const app = createApp()
const res = await app.request('/simulations', {
@ -199,7 +219,7 @@ describe('POST /simulations', () => {
expect(body.id).toBe('prod-3')
expect(body.tache).toBe('EO_T2_LIVE')
expect(body.sujet).toBeNull()
// 2 appels from : profiles (auth) + productions (insert) — pas de sujets, pas de update
// 2 appels from : profiles (auth) + productions (insert) — pas de sujets
expect(vi.mocked(supabase.from).mock.calls).toHaveLength(2)
})
@ -240,8 +260,8 @@ describe('POST /simulations', () => {
it('aucun sujet actif trouvé → création OK avec sujet: null (non bloquant)', async () => {
const profile = buildProfile({ plan: 'standard', simulations_used: 10 })
mockAuth(profile)
mockInsert({ id: 'prod-4', tache: 'EE_T3', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
mockSujets([]) // table vide pour ce filtre
mockInsert({ id: 'prod-4', tache: 'EE_T3', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
const app = createApp()
const res = await app.request('/simulations', {
@ -255,7 +275,7 @@ describe('POST /simulations', () => {
expect(body.id).toBe('prod-4')
expect(body.sujet).toBeNull()
expect(vi.mocked(supabase.from).mock.calls).toHaveLength(3)
expect(vi.mocked(supabase.from).mock.calls[2][0]).toBe('sujets')
expect(vi.mocked(supabase.from).mock.calls[1][0]).toBe('sujets')
})
it('pick aléatoire parmi plusieurs sujets actifs', async () => {
@ -266,8 +286,8 @@ describe('POST /simulations', () => {
{ ...MOCK_SUJET_EE_T1, id: 'sujet-c' },
]
mockAuth(profile)
mockInsert({ id: 'prod-5', tache: 'EE_T1', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
mockSujets(candidates)
mockInsert({ id: 'prod-5', tache: 'EE_T1', mode: 'entrainement', created_at: '2024-01-01T00:00:00Z' })
const app = createApp()
const res = await app.request('/simulations', {
@ -304,7 +324,7 @@ describe('GET /simulations/:id', () => {
exercices: ['exo 1'],
}
it('OK : rapport trouvé et appartenant à l\'utilisateur → 200 avec simulation_id', async () => {
it('OK (avec sujet) : rapport trouvé, appartenant à l\'utilisateur → 200', async () => {
const profile = buildProfile({ id: 'user-123', plan: 'standard' })
mockAuth(profile)
mockProductionSelect({
@ -312,11 +332,12 @@ describe('GET /simulations/:id', () => {
user_id: 'user-123',
tache: 'EE_T1',
mode: 'entrainement',
score: 14,
nclc: 8,
contenu: 'Mon texte en cours.',
sujet_id: 'sujet-1',
rapport: JSON.stringify(VALID_RAPPORT),
created_at: '2024-01-01T00:00:00Z',
})
mockSujetById(MOCK_SUJET_EE_T1)
const app = createApp()
const res = await app.request('/simulations/prod-42', {
@ -329,14 +350,65 @@ describe('GET /simulations/:id', () => {
expect(body.tache).toBe('EE_T1')
expect(body.mode).toBe('entrainement')
expect(body.created_at).toBe('2024-01-01T00:00:00Z')
expect(body.score).toBe(14)
expect(body.nclc).toBe(8)
expect(body.feedback_court).toBe('Bonne production générale.')
expect(body.criteres).toHaveLength(4)
expect(body.erreurs).toEqual(['erreur 1'])
expect(body.modele).toBe('Texte modèle.')
expect(body.idees).toEqual(['idée 1'])
expect(body.exercices).toEqual(['exo 1'])
expect(body.contenu).toBe('Mon texte en cours.')
expect(body.sujet).toEqual(MOCK_SUJET_EE_T1)
expect(body.rapport).toEqual(VALID_RAPPORT)
})
it('OK (sans sujet_id) : production sans sujet → sujet: null', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect({
id: 'prod-42',
user_id: 'user-123',
tache: 'EO_T2_LIVE',
mode: 'entrainement',
contenu: null,
sujet_id: null,
rapport: JSON.stringify(VALID_RAPPORT),
created_at: '2024-01-01T00:00:00Z',
})
const app = createApp()
const res = await app.request('/simulations/prod-42', {
headers: { Authorization: 'Bearer token' },
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.sujet).toBeNull()
expect(body.contenu).toBeNull()
expect(body.rapport).toEqual(VALID_RAPPORT)
// Pas d'appel from('sujets') : sujet_id null
expect(vi.mocked(supabase.from).mock.calls).toHaveLength(2)
})
it('FTD-21 — simulation en cours : rapport=null retourné avec contenu + sujet (resume)', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect({
id: 'prod-42',
user_id: 'user-123',
tache: 'EE_T1',
mode: 'entrainement',
contenu: 'Brouillon en cours.',
sujet_id: 'sujet-1',
rapport: null,
created_at: '2024-01-01T00:00:00Z',
})
mockSujetById(MOCK_SUJET_EE_T1)
const app = createApp()
const res = await app.request('/simulations/prod-42', {
headers: { Authorization: 'Bearer token' },
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.simulation_id).toBe('prod-42')
expect(body.contenu).toBe('Brouillon en cours.')
expect(body.sujet).toEqual(MOCK_SUJET_EE_T1)
expect(body.rapport).toBeNull()
})
it('SIMULATION_NOT_FOUND : id inexistant → 404', async () => {
@ -363,8 +435,8 @@ describe('GET /simulations/:id', () => {
user_id: 'another-user-456',
tache: 'EE_T1',
mode: 'entrainement',
score: 14,
nclc: 8,
contenu: null,
sujet_id: null,
rapport: JSON.stringify(VALID_RAPPORT),
created_at: '2024-01-01T00:00:00Z',
})
@ -379,29 +451,188 @@ describe('GET /simulations/:id', () => {
expect(body.error).toBe(true)
expect(body.code).toBe('AUTH_REQUIRED')
})
})
it('REPORT_NOT_READY : rapport === null → 404', async () => {
describe('PATCH /simulations/:id/contenu — FTD-21 autosave', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('succès : contenu sauvegardé → 200 { ok: true }', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect({
id: 'prod-42',
user_id: 'user-123',
tache: 'EE_T1',
mode: 'entrainement',
score: null,
nclc: null,
rapport: null,
created_at: '2024-01-01T00:00:00Z',
})
mockProductionSelect({ user_id: 'user-123', rapport: null })
mockUpdate()
const app = createApp()
const res = await app.request('/simulations/prod-42', {
headers: { Authorization: 'Bearer token' },
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ contenu: 'Mon brouillon.' }),
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.ok).toBe(true)
})
it('AUTH_REQUIRED : simulation appartient à un autre user → 401', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect({ user_id: 'another-user', rapport: null })
const app = createApp()
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ contenu: 'Texte.' }),
})
expect(res.status).toBe(401)
const body = await res.json()
expect(body.code).toBe('AUTH_REQUIRED')
})
it('SIMULATION_NOT_FOUND : id inexistant → 404', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect(null, { message: 'No rows' })
const app = createApp()
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ contenu: 'Texte.' }),
})
expect(res.status).toBe(404)
const body = await res.json()
expect(body.error).toBe(true)
expect(body.code).toBe('REPORT_NOT_READY')
expect(body.code).toBe('SIMULATION_NOT_FOUND')
})
it('VALIDATION_ERROR : contenu > 5000 caractères → 400', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
// Pas besoin de mocks supabase : la validation bloque avant tout accès DB
const app = createApp()
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ contenu: 'a'.repeat(5001) }),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body.code).toBe('VALIDATION_ERROR')
})
it('VALIDATION_ERROR : simulation déjà corrigée → 400', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockProductionSelect({ user_id: 'user-123', rapport: '{"score":14}' })
const app = createApp()
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ contenu: 'Texte.' }),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body.code).toBe('VALIDATION_ERROR')
})
it('VALIDATION_ERROR : body.contenu manquant → 400', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
const app = createApp()
const res = await app.request('/simulations/prod-42/contenu', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body.code).toBe('VALIDATION_ERROR')
})
})
describe('PATCH /simulations/:id/sujet — FTD-21 changement de sujet', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('succès : sujet mis à jour → 200 avec sujet complet', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockSujetById(MOCK_SUJET_EE_T1)
mockProductionSelect({ user_id: 'user-123', rapport: null })
mockUpdate()
const app = createApp()
const res = await app.request('/simulations/prod-42/sujet', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ sujet_id: 'sujet-1' }),
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.sujet).toEqual(MOCK_SUJET_EE_T1)
})
it('SUJET_NOT_FOUND : sujet_id inexistant → 404', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockSujetById(null, { message: 'No rows' })
const app = createApp()
const res = await app.request('/simulations/prod-42/sujet', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ sujet_id: 'does-not-exist' }),
})
expect(res.status).toBe(404)
const body = await res.json()
expect(body.code).toBe('SUJET_NOT_FOUND')
})
it('AUTH_REQUIRED : simulation appartient à un autre user → 401', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
mockSujetById(MOCK_SUJET_EE_T1)
mockProductionSelect({ user_id: 'another-user', rapport: null })
const app = createApp()
const res = await app.request('/simulations/prod-42/sujet', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({ sujet_id: 'sujet-1' }),
})
expect(res.status).toBe(401)
const body = await res.json()
expect(body.code).toBe('AUTH_REQUIRED')
})
it('VALIDATION_ERROR : sujet_id manquant → 400', async () => {
const profile = buildProfile({ id: 'user-123' })
mockAuth(profile)
const app = createApp()
const res = await app.request('/simulations/prod-42/sujet', {
method: 'PATCH',
headers: { Authorization: 'Bearer token', 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body.code).toBe('VALIDATION_ERROR')
})
})

View file

@ -75,29 +75,8 @@ export async function create(
}
}
// 2. Insérer dans productions
const { data, error } = await supabase
.from('productions')
.insert({
user_id: profile.id,
tache: body.tache,
mode: body.mode,
contenu: body.contenu ?? null,
})
.select('id, tache, mode, created_at')
.single()
if (error || !data) {
return {
error: true,
code: 'INTERNAL_ERROR',
message: 'Une erreur est survenue. Veuillez réessayer dans quelques instants.',
status: 500,
}
}
// 3. Fetch un sujet aléatoire (non bloquant — sujet: null si introuvable).
// TODO: migrer vers une RPC PostgreSQL si la table sujets dépasse quelques centaines de lignes.
// 2. Fetch un sujet aléatoire AVANT l'insert pour persister sujet_id en une seule requête.
// (non bloquant — sujet: null si introuvable).
const sujetParams = mapTacheToSujetParams(body.tache)
let sujet: SujetData | null = null
if (sujetParams) {
@ -113,6 +92,28 @@ export async function create(
}
}
// 3. Insérer dans productions avec sujet_id (FTD-21 — persistance pour resume).
const { data, error } = await supabase
.from('productions')
.insert({
user_id: profile.id,
tache: body.tache,
mode: body.mode,
contenu: body.contenu ?? null,
sujet_id: sujet?.id ?? null,
})
.select('id, tache, mode, created_at')
.single()
if (error || !data) {
return {
error: true,
code: 'INTERNAL_ERROR',
message: 'Une erreur est survenue. Veuillez réessayer dans quelques instants.',
status: 500,
}
}
return {
data: {
id: data.id,
@ -126,14 +127,22 @@ export async function create(
// EERapport et EORapport ont la même structure depuis l'étape A —
// on utilise EERapport comme représentation canonique du rapport parsé.
export type GetByIdResult = EERapport & {
//
// FTD-21 : rapport peut être null (simulation en cours, pas encore corrigée).
// Le frontend distingue :
// - rapport !== null → RapportPage affiche la correction
// - rapport === null → SimulationFlowProvider restaure la session (resume)
export interface GetByIdResult {
simulation_id: string
tache: Tache
mode: Mode
created_at: string
contenu: string | null
sujet: SujetData | null
rapport: EERapport | null
}
type GetByIdError = {
type ControllerError = {
error: true
code: string
message: string
@ -143,10 +152,10 @@ type GetByIdError = {
export async function getById(
id: string,
profile: AuthProfile
): Promise<{ data: GetByIdResult } | GetByIdError> {
): Promise<{ data: GetByIdResult } | ControllerError> {
const { data, error } = await supabase
.from('productions')
.select('id, user_id, tache, mode, score, nclc, rapport, created_at')
.select('id, user_id, tache, mode, contenu, sujet_id, rapport, created_at')
.eq('id', id)
.single()
@ -168,24 +177,170 @@ export async function getById(
}
}
if (data.rapport === null) {
return {
error: true,
code: 'REPORT_NOT_READY',
message: "Le rapport n'est pas encore disponible pour cette simulation.",
status: 404,
}
// Charger le sujet si présent (FTD-21 — restore complet de la session).
let sujet: SujetData | null = null
if (data.sujet_id) {
const { data: sujetRow } = await supabase
.from('sujets')
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
.eq('id', data.sujet_id)
.single()
if (sujetRow) sujet = sujetRow as SujetData
}
const rapport = JSON.parse(data.rapport) as EERapport
const rapport = data.rapport ? (JSON.parse(data.rapport) as EERapport) : null
return {
data: {
...rapport,
simulation_id: data.id,
tache: data.tache as Tache,
mode: data.mode as Mode,
created_at: data.created_at,
contenu: data.contenu ?? null,
sujet,
rapport,
},
}
}
/**
* FTD-21 autosave du contenu d'une simulation en cours.
* Refuse si la simulation est déjà corrigée (rapport !== null).
*/
export async function autosaveContenu(
id: string,
userId: string,
contenu: string
): Promise<{ data: { ok: true } } | ControllerError> {
if (contenu.length > 5000) {
return {
error: true,
code: 'VALIDATION_ERROR',
message: 'Le texte ne doit pas dépasser 5 000 caractères.',
status: 400,
}
}
const { data: prod, error } = await supabase
.from('productions')
.select('user_id, rapport')
.eq('id', id)
.single()
if (error || !prod) {
return {
error: true,
code: 'SIMULATION_NOT_FOUND',
message: 'Simulation introuvable.',
status: 404,
}
}
if (prod.user_id !== userId) {
return {
error: true,
code: 'AUTH_REQUIRED',
message: 'Cette simulation ne vous appartient pas.',
status: 401,
}
}
if (prod.rapport !== null) {
return {
error: true,
code: 'VALIDATION_ERROR',
message: 'Cette simulation a déjà été corrigée.',
status: 400,
}
}
const { error: updateError } = await supabase
.from('productions')
.update({ contenu })
.eq('id', id)
if (updateError) {
return {
error: true,
code: 'INTERNAL_ERROR',
message: 'Sauvegarde impossible. Réessayez dans quelques instants.',
status: 500,
}
}
return { data: { ok: true } }
}
/**
* FTD-21 met à jour le sujet d'une simulation en cours.
* Vérifie que le sujet existe et que la simulation n'est pas corrigée.
*/
export async function updateSujet(
id: string,
userId: string,
sujetId: string
): Promise<{ data: { sujet: SujetData } } | ControllerError> {
const { data: sujetRow, error: sujetError } = await supabase
.from('sujets')
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
.eq('id', sujetId)
.single()
if (sujetError || !sujetRow) {
return {
error: true,
code: 'SUJET_NOT_FOUND',
message: 'Sujet introuvable.',
status: 404,
}
}
const { data: prod, error } = await supabase
.from('productions')
.select('user_id, rapport')
.eq('id', id)
.single()
if (error || !prod) {
return {
error: true,
code: 'SIMULATION_NOT_FOUND',
message: 'Simulation introuvable.',
status: 404,
}
}
if (prod.user_id !== userId) {
return {
error: true,
code: 'AUTH_REQUIRED',
message: 'Cette simulation ne vous appartient pas.',
status: 401,
}
}
if (prod.rapport !== null) {
return {
error: true,
code: 'VALIDATION_ERROR',
message: 'Cette simulation a déjà été corrigée.',
status: 400,
}
}
const { error: updateError } = await supabase
.from('productions')
.update({ sujet_id: sujetId })
.eq('id', id)
if (updateError) {
return {
error: true,
code: 'INTERNAL_ERROR',
message: 'Mise à jour impossible. Réessayez dans quelques instants.',
status: 500,
}
}
return { data: { sujet: sujetRow as SujetData } }
}

View file

@ -92,4 +92,74 @@ simulations.get('/:id', authMiddleware, async (c) => {
return c.json(result.data, 200)
})
// FTD-21 — autosave du contenu d'une simulation en cours.
simulations.patch('/:id/contenu', authMiddleware, async (c) => {
const id = c.req.param('id')!
let body: { contenu?: 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 (typeof body.contenu !== 'string') {
return c.json(
{
error: true,
code: 'VALIDATION_ERROR',
message: 'Le champ `contenu` est requis et doit être une chaîne.',
},
400
)
}
const profile = c.get('profile')
const result = await simulationController.autosaveContenu(id, profile.id, body.contenu)
if ('error' in result) {
return c.json(result, result.status as 400 | 401 | 404 | 500)
}
return c.json(result.data, 200)
})
// FTD-21 — met à jour le sujet d'une simulation en cours.
simulations.patch('/:id/sujet', authMiddleware, async (c) => {
const id = c.req.param('id')!
let body: { sujet_id?: 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.sujet_id || typeof body.sujet_id !== 'string') {
return c.json(
{
error: true,
code: 'VALIDATION_ERROR',
message: 'Le champ `sujet_id` est requis et doit être une chaîne.',
},
400
)
}
const profile = c.get('profile')
const result = await simulationController.updateSujet(id, profile.id, body.sujet_id)
if ('error' in result) {
return c.json(result, result.status as 400 | 401 | 404 | 500)
}
return c.json(result.data, 200)
})
export default simulations