11import  {  FastifyPluginAsync  }  from  "fastify" ; 
22import  rateLimiter  from  "api/plugins/rateLimiter.js" ; 
33import  { 
4+   roomGetResponse , 
45  roomRequestBaseSchema , 
56  RoomRequestFormValues , 
67  roomRequestPostResponse , 
@@ -17,7 +18,11 @@ import {
1718  DatabaseInsertError , 
1819  InternalServerError , 
1920}  from  "common/errors/index.js" ; 
20- import  {  PutItemCommand ,  QueryCommand  }  from  "@aws-sdk/client-dynamodb" ; 
21+ import  { 
22+   PutItemCommand , 
23+   QueryCommand , 
24+   TransactWriteItemsCommand , 
25+ }  from  "@aws-sdk/client-dynamodb" ; 
2126import  {  genericConfig  }  from  "common/config.js" ; 
2227import  {  marshall ,  unmarshall  }  from  "@aws-sdk/util-dynamodb" ; 
2328import  {  z  }  from  "zod" ; 
@@ -81,11 +86,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
8186    { 
8287      schema : { 
8388        response : { 
84-           200 : zodToJsonSchema ( 
85-             z . array ( 
86-               roomRequestBaseSchema . extend ( {  requestId : z . string ( ) . uuid ( )  } ) , 
87-             ) , 
88-           ) , 
89+           200 : zodToJsonSchema ( roomGetResponse ) , 
8990        } , 
9091      } , 
9192      onRequest : async  ( request ,  reply )  =>  { 
@@ -116,7 +117,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
116117          ExpressionAttributeNames : { 
117118            "#hashKey" : "userId#requestId" , 
118119          } , 
119-           ProjectionExpression : "requestId, host, title" , 
120+           ProjectionExpression : "requestId, host, title, semester " , 
120121          ExpressionAttributeValues : { 
121122            ":semesterValue" : {  S : semesterId  } , 
122123            ":username" : {  S : request . username  } , 
@@ -129,8 +130,49 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
129130          message : "Could not get room requests." , 
130131        } ) ; 
131132      } 
132-       const  items  =  response . Items . map ( ( x )  =>  unmarshall ( x ) ) ; 
133-       return  reply . status ( 200 ) . send ( items ) ; 
133+       const  items  =  response . Items . map ( ( x )  =>  { 
134+         const  item  =  unmarshall ( x )  as  { 
135+           host : string ; 
136+           title : string ; 
137+           requestId : string ; 
138+           status : string ; 
139+         } ; 
140+         const  statusPromise  =  fastify . dynamoClient . send ( 
141+           new  QueryCommand ( { 
142+             TableName : genericConfig . RoomRequestsStatusTableName , 
143+             KeyConditionExpression : "requestId = :requestId" , 
144+             ExpressionAttributeValues : { 
145+               ":requestId" : {  S : item . requestId  } , 
146+             } , 
147+             ProjectionExpression : "#status" , 
148+             ExpressionAttributeNames : { 
149+               "#status" : "status" , 
150+             } , 
151+             ScanIndexForward : false , 
152+             Limit : 1 , 
153+           } ) , 
154+         ) ; 
155+ 
156+         return  statusPromise . then ( ( statusResponse )  =>  { 
157+           if  ( 
158+             ! statusResponse  || 
159+             ! statusResponse . Items  || 
160+             statusResponse . Items . length  ==  0 
161+           )  { 
162+             return  "unknown" ; 
163+           } 
164+           const  statuses  =  statusResponse . Items . map ( ( s )  =>  unmarshall ( s ) ) ; 
165+           const  latestStatus  =  statuses . length  >  0  ? statuses [ 0 ] . status  : null ; 
166+           return  { 
167+             ...item , 
168+             status : latestStatus , 
169+           } ; 
170+         } ) ; 
171+       } ) ; 
172+ 
173+       const  itemsWithStatus  =  await  Promise . all ( items ) ; 
174+ 
175+       return  reply . status ( 200 ) . send ( itemsWithStatus ) ; 
134176    } , 
135177  ) ; 
136178  fastify . post < {  Body : RoomRequestFormValues  } > ( 
@@ -161,12 +203,31 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
161203        semesterId : request . body . semester , 
162204      } ; 
163205      try  { 
164-         await  fastify . dynamoClient . send ( 
165-           new  PutItemCommand ( { 
166-             TableName : genericConfig . RoomRequestsTableName , 
167-             Item : marshall ( body ) , 
168-           } ) , 
169-         ) ; 
206+         const  createdAt  =  new  Date ( ) . toISOString ( ) ; 
207+         const  transactionCommand  =  new  TransactWriteItemsCommand ( { 
208+           TransactItems : [ 
209+             { 
210+               Put : { 
211+                 TableName : genericConfig . RoomRequestsTableName , 
212+                 Item : marshall ( body ) , 
213+               } , 
214+             } , 
215+             { 
216+               Put : { 
217+                 TableName : genericConfig . RoomRequestsStatusTableName , 
218+                 Item : marshall ( { 
219+                   requestId, 
220+                   semesterId : request . body . semester , 
221+                   "createdAt#status" : `${ createdAt }  #${ RoomRequestStatus . CREATED }  ` , 
222+                   createdBy : request . username , 
223+                   status : RoomRequestStatus . CREATED , 
224+                   notes : "This request was created by the user." , 
225+                 } ) , 
226+               } , 
227+             } , 
228+           ] , 
229+         } ) ; 
230+         await  fastify . dynamoClient . send ( transactionCommand ) ; 
170231      }  catch  ( e )  { 
171232        if  ( e  instanceof  BaseError )  { 
172233          throw  e ; 
@@ -182,6 +243,100 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
182243      } ) ; 
183244    } , 
184245  ) ; 
246+   fastify . get < { 
247+     Body : undefined ; 
248+     Params : {  requestId : string ;  semesterId : string  } ; 
249+   } > ( 
250+     "/:semesterId/:requestId" , 
251+     { 
252+       onRequest : async  ( request ,  reply )  =>  { 
253+         await  fastify . authorize ( request ,  reply ,  [ AppRoles . ROOM_REQUEST_CREATE ] ) ; 
254+       } , 
255+     } , 
256+     async  ( request ,  reply )  =>  { 
257+       const  requestId  =  request . params . requestId ; 
258+       const  semesterId  =  request . params . semesterId ; 
259+       let  command ; 
260+       if  ( request . userRoles ?. has ( AppRoles . BYPASS_OBJECT_LEVEL_AUTH ) )  { 
261+         command  =  new  QueryCommand ( { 
262+           TableName : genericConfig . RoomRequestsTableName , 
263+           IndexName : "RequestIdIndex" , 
264+           KeyConditionExpression : "requestId = :requestId" , 
265+           FilterExpression : "semesterId = :semesterId" , 
266+           ExpressionAttributeValues : { 
267+             ":requestId" : {  S : requestId  } , 
268+             ":semesterId" : {  S : semesterId  } , 
269+           } , 
270+           Limit : 1 , 
271+         } ) ; 
272+       }  else  { 
273+         command  =  new  QueryCommand ( { 
274+           TableName : genericConfig . RoomRequestsTableName , 
275+           KeyConditionExpression :
276+             "semesterId = :semesterId AND #userIdRequestId = :userRequestId" , 
277+           ExpressionAttributeValues : { 
278+             ":userRequestId" : {  S : `${ request . username }  #${ requestId }  `  } , 
279+             ":semesterId" : {  S : semesterId  } , 
280+           } , 
281+           ExpressionAttributeNames : { 
282+             "#userIdRequestId" : "userId#requestId" , 
283+           } , 
284+           Limit : 1 , 
285+         } ) ; 
286+       } 
287+       try  { 
288+         const  resp  =  await  fastify . dynamoClient . send ( command ) ; 
289+         if  ( ! resp . Items  ||  resp . Count  !=  1 )  { 
290+           throw  new  DatabaseFetchError ( { 
291+             message : "Recieved no response." , 
292+           } ) ; 
293+         } 
294+         // this isn't atomic, but that's fine - a little inconsistency on this isn't a problem. 
295+         try  { 
296+           const  statusesResponse  =  await  fastify . dynamoClient . send ( 
297+             new  QueryCommand ( { 
298+               TableName : genericConfig . RoomRequestsStatusTableName , 
299+               KeyConditionExpression : "requestId = :requestId" , 
300+               ExpressionAttributeValues : { 
301+                 ":requestId" : {  S : requestId  } , 
302+               } , 
303+               ProjectionExpression : "#createdAt,#notes,#createdBy" , 
304+               ExpressionAttributeNames : { 
305+                 "#createdBy" : "createdBy" , 
306+                 "#createdAt" : "createdAt#status" , 
307+                 "#notes" : "notes" , 
308+               } , 
309+             } ) , 
310+           ) ; 
311+           const  updates  =  statusesResponse . Items ?. map ( ( x )  =>  { 
312+             const  unmarshalled  =  unmarshall ( x ) ; 
313+             return  { 
314+               createdBy : unmarshalled [ "createdBy" ] , 
315+               createdAt : unmarshalled [ "createdAt#status" ] . split ( "#" ) [ 0 ] , 
316+               status : unmarshalled [ "createdAt#status" ] . split ( "#" ) [ 1 ] , 
317+               notes : unmarshalled [ "notes" ] , 
318+             } ; 
319+           } ) ; 
320+           return  reply 
321+             . status ( 200 ) 
322+             . send ( {  data : unmarshall ( resp . Items [ 0 ] ) ,  updates } ) ; 
323+         }  catch  ( e )  { 
324+           request . log . error ( e ) ; 
325+           throw  new  DatabaseFetchError ( { 
326+             message : "Could not get request status." , 
327+           } ) ; 
328+         } 
329+       }  catch  ( e )  { 
330+         request . log . error ( e ) ; 
331+         if  ( e  instanceof  BaseError )  { 
332+           throw  e ; 
333+         } 
334+         throw  new  DatabaseInsertError ( { 
335+           message : "Could not find by ID." , 
336+         } ) ; 
337+       } 
338+     } , 
339+   ) ; 
185340} ; 
186341
187342export  default  roomRequestRoutes ; 
0 commit comments