11import type { EventType } from '@prisma/client' ;
2+ import { Prisma } from '@prisma/client' ;
23
34import type { NextFunction , Request , Response } from 'express' ;
45
56import { prisma } from '../prisma.js' ;
7+ import { NotFoundError } from '../utils/AppError.js' ;
8+ import { makeItemKey } from '../utils/itemKey.js' ;
69import { getTodayTimeWindow } from '../utils/time.js' ;
710
11+ const SERIALIZABLE_TX_MAX_ATTEMPTS = 5 ;
12+
13+ const isSerializationFailure = ( err : unknown ) : boolean =>
14+ err instanceof Prisma . PrismaClientKnownRequestError && err . code === 'P2034' ;
15+
816export const getMe = async ( req : Request , res : Response ) => {
917 const { userId } = req . user ! ;
1018
@@ -17,6 +25,8 @@ export const getMe = async (req: Request, res: Response) => {
1725 reports : true ,
1826 favoritedEateries : true ,
1927 favoritedItemNames : true ,
28+ likedItemKeys : true ,
29+ dislikedItemKeys : true ,
2030 userEventVotes : true ,
2131 } ,
2232 } ) ;
@@ -65,6 +75,112 @@ export const removeFcmToken = async (
6575 }
6676} ;
6777
78+ export const setItemPreference = async (
79+ req : Request ,
80+ res : Response ,
81+ next : NextFunction ,
82+ ) => {
83+ try {
84+ const { name, cornellId, preference } = req . body as {
85+ name : string ;
86+ cornellId : number ;
87+ preference : 'liked' | 'disliked' | 'none' ;
88+ } ;
89+ const { userId } = req . user ! ;
90+
91+ const itemKey = makeItemKey ( name , cornellId ) ;
92+
93+ for ( let attempt = 0 ; attempt < SERIALIZABLE_TX_MAX_ATTEMPTS ; attempt ++ ) {
94+ try {
95+ await prisma . $transaction (
96+ async ( tx ) => {
97+ const user = await tx . user . findUnique ( {
98+ where : { id : userId } ,
99+ select : {
100+ id : true ,
101+ likedItemKeys : true ,
102+ dislikedItemKeys : true ,
103+ } ,
104+ } ) ;
105+
106+ if ( ! user ) {
107+ throw new NotFoundError ( 'User not found.' ) ;
108+ }
109+
110+ const likedSet = new Set ( user . likedItemKeys ?? [ ] ) ;
111+ const dislikedSet = new Set ( user . dislikedItemKeys ?? [ ] ) ;
112+ const previousPreference : 'liked' | 'disliked' | 'none' =
113+ likedSet . has ( itemKey )
114+ ? 'liked'
115+ : dislikedSet . has ( itemKey )
116+ ? 'disliked'
117+ : 'none' ;
118+
119+ likedSet . delete ( itemKey ) ;
120+ dislikedSet . delete ( itemKey ) ;
121+
122+ if ( preference === 'liked' ) {
123+ likedSet . add ( itemKey ) ;
124+ } else if ( preference === 'disliked' ) {
125+ dislikedSet . add ( itemKey ) ;
126+ }
127+
128+ const likeDelta =
129+ ( preference === 'liked' ? 1 : 0 ) -
130+ ( previousPreference === 'liked' ? 1 : 0 ) ;
131+ const dislikeDelta =
132+ ( preference === 'disliked' ? 1 : 0 ) -
133+ ( previousPreference === 'disliked' ? 1 : 0 ) ;
134+
135+ await tx . user . update ( {
136+ where : { id : user . id } ,
137+ data : {
138+ likedItemKeys : Array . from ( likedSet ) ,
139+ dislikedItemKeys : Array . from ( dislikedSet ) ,
140+ } ,
141+ } ) ;
142+
143+ if ( likeDelta !== 0 || dislikeDelta !== 0 ) {
144+ // Create count row if missing
145+ await tx . itemPreferenceCounts . upsert ( {
146+ where : { itemKey } ,
147+ create : {
148+ itemKey,
149+ numLikes : 0 ,
150+ numDislikes : 0 ,
151+ } ,
152+ update : { } ,
153+ } ) ;
154+ // Apply deltas
155+ await tx . itemPreferenceCounts . update ( {
156+ where : { itemKey } ,
157+ data : {
158+ numLikes : { increment : likeDelta } ,
159+ numDislikes : { increment : dislikeDelta } ,
160+ } ,
161+ } ) ;
162+ }
163+ } ,
164+ { isolationLevel : Prisma . TransactionIsolationLevel . Serializable } ,
165+ ) ;
166+ break ;
167+ } catch ( err ) {
168+ if (
169+ isSerializationFailure ( err ) &&
170+ attempt < SERIALIZABLE_TX_MAX_ATTEMPTS - 1
171+ ) {
172+ continue ;
173+ }
174+ throw err ;
175+ }
176+ }
177+
178+ return res . status ( 200 ) . json ( { message : 'Item preference updated.' } ) ;
179+ } catch ( error ) {
180+ return next ( error ) ;
181+ }
182+ } ;
183+
68184export const addFavoriteItem = async (
69185 req : Request ,
70186 res : Response ,
0 commit comments