From 0abd2eb45cd0b5bd69cc1ccede6f8d89e21f49eb Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Wed, 24 Jun 2026 15:36:46 +0200 Subject: [PATCH] mcp --- .../src/dashboard/dashboard.service.spec.ts | 84 ++++++++++++++ .../src/dashboard/dashboard.service.ts | 108 +++++++++++++++++- 2 files changed, 188 insertions(+), 4 deletions(-) diff --git a/listify-api/src/dashboard/dashboard.service.spec.ts b/listify-api/src/dashboard/dashboard.service.spec.ts index 4c1619d..d82562b 100644 --- a/listify-api/src/dashboard/dashboard.service.spec.ts +++ b/listify-api/src/dashboard/dashboard.service.spec.ts @@ -128,6 +128,70 @@ describe('DashboardService', () => { expect(secondResponse.weeklySuggestions.suggestions).toHaveLength(1); }); + it('loads the existing weekly snapshot when a parallel request already created it', async () => { + const { weekKey } = currentKeys(); + const existingSnapshot: WeeklyListSuggestionSnapshotEntity = { + id: 'weekly-existing', + userId: 'user-1', + weekKey, + timezone: 'UTC', + suggestions: [ + { + id: 'suggestion-existing', + title: 'Meal Prep', + items: [{ title: 'Rezepte auswaehlen', required: true }], + reason: 'Schon gespeichert.', + }, + ], + requestPayload: {}, + responsePayload: null, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + listsService.listLists.mockResolvedValue([createList('list-1', 'Einkauf')]); + await saveCurrentDailySnapshot(dailySnapshotsRepository, 'user-1'); + jest + .spyOn(weeklySnapshotsRepository, 'findOne') + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(existingSnapshot); + jest.spyOn(weeklySnapshotsRepository, 'save').mockRejectedValueOnce({ + code: 'ER_DUP_ENTRY', + errno: 1062, + message: + "Duplicate entry 'user-1-2026-W26' for key 'weekly_list_suggestion_snapshots.IDX_test'", + }); + mockMistralResponses([ + { + choices: [ + { + message: { + content: JSON.stringify({ + suggestions: [ + { + title: 'Meal Prep', + items: [{ title: 'Rezepte auswaehlen', required: true }], + reason: 'Parallel erzeugt.', + }, + ], + }), + }, + }, + ], + }, + ]); + + const response = await service.getDashboard('user-1'); + + expect(response.weeklySuggestions.suggestions).toEqual([ + expect.objectContaining({ + id: 'suggestion-existing', + title: 'Meal Prep', + }), + ]); + }); + it('keeps dashboard snapshots isolated per user', async () => { listsService.listLists.mockImplementation((userId: string) => Promise.resolve([createList(`${userId}-list`, `Liste ${userId}`)]), @@ -357,6 +421,26 @@ async function saveCurrentWeeklySnapshot( }); } +async function saveCurrentDailySnapshot( + repository: InMemoryRepository, + userId: string, +): Promise { + const { dateKey } = currentKeys(); + + await repository.save({ + id: `${userId}-daily`, + userId, + dateKey, + timezone: 'UTC', + selectedLists: [], + requestPayload: {}, + responsePayload: null, + errorMessage: null, + createdAt: new Date(), + updatedAt: new Date(), + }); +} + function currentKeys(): { dateKey: string; weekKey: string } { const now = new Date(); const year = now.getUTCFullYear(); diff --git a/listify-api/src/dashboard/dashboard.service.ts b/listify-api/src/dashboard/dashboard.service.ts index a13ada1..39b9623 100644 --- a/listify-api/src/dashboard/dashboard.service.ts +++ b/listify-api/src/dashboard/dashboard.service.ts @@ -176,7 +176,9 @@ export class DashboardService { try { const result = await this.selectImportantListsWithAi(visibleLists); - return this.dailySnapshotsRepository.save( + return this.saveOrLoadDailySnapshot( + userId, + dateKey, this.dailySnapshotsRepository.create({ id: randomUUID(), userId, @@ -189,7 +191,9 @@ export class DashboardService { }), ); } catch (error) { - return this.dailySnapshotsRepository.save( + return this.saveOrLoadDailySnapshot( + userId, + dateKey, this.dailySnapshotsRepository.create({ id: randomUUID(), userId, @@ -223,7 +227,9 @@ export class DashboardService { try { const result = await this.createWeeklySuggestionsWithAi(visibleLists); - return this.weeklySnapshotsRepository.save( + return this.saveOrLoadWeeklySnapshot( + userId, + weekKey, this.weeklySnapshotsRepository.create({ id: randomUUID(), userId, @@ -236,7 +242,9 @@ export class DashboardService { }), ); } catch (error) { - return this.weeklySnapshotsRepository.save( + return this.saveOrLoadWeeklySnapshot( + userId, + weekKey, this.weeklySnapshotsRepository.create({ id: randomUUID(), userId, @@ -255,6 +263,70 @@ export class DashboardService { } } + private async saveOrLoadDailySnapshot( + userId: string, + dateKey: string, + snapshot: DailyDashboardSnapshotEntity, + ): Promise { + try { + return await this.dailySnapshotsRepository.save(snapshot); + } catch (error) { + if (!this.isDuplicateSnapshotError(error)) { + throw error; + } + + return this.requireDailySnapshot(userId, dateKey); + } + } + + private async saveOrLoadWeeklySnapshot( + userId: string, + weekKey: string, + snapshot: WeeklyListSuggestionSnapshotEntity, + ): Promise { + try { + return await this.weeklySnapshotsRepository.save(snapshot); + } catch (error) { + if (!this.isDuplicateSnapshotError(error)) { + throw error; + } + + return this.requireWeeklySnapshot(userId, weekKey); + } + } + + private async requireDailySnapshot( + userId: string, + dateKey: string, + ): Promise { + const snapshot = await this.dailySnapshotsRepository.findOne({ + where: { userId, dateKey }, + }); + + if (!snapshot) { + throw new Error('Daily dashboard snapshot was not found after conflict.'); + } + + return snapshot; + } + + private async requireWeeklySnapshot( + userId: string, + weekKey: string, + ): Promise { + const snapshot = await this.weeklySnapshotsRepository.findOne({ + where: { userId, weekKey }, + }); + + if (!snapshot) { + throw new Error( + 'Weekly list suggestion snapshot was not found after conflict.', + ); + } + + return snapshot; + } + private async selectImportantListsWithAi( lists: UserList[], ): Promise> { @@ -871,4 +943,32 @@ export class DashboardService { private errorResponsePayload(error: unknown): unknown { return error instanceof DashboardAiCallError ? error.responsePayload : null; } + + private isDuplicateSnapshotError(error: unknown): boolean { + if (!error || typeof error !== 'object') { + return false; + } + + const candidate = error as { + code?: unknown; + errno?: unknown; + message?: unknown; + driverError?: { + code?: unknown; + errno?: unknown; + message?: unknown; + }; + }; + const code = candidate.code ?? candidate.driverError?.code; + const errno = candidate.errno ?? candidate.driverError?.errno; + const message = String( + candidate.message ?? candidate.driverError?.message ?? '', + ); + + return ( + code === 'ER_DUP_ENTRY' || + errno === 1062 || + message.includes('Duplicate entry') + ); + } }