@@ -43,7 +43,50 @@ import {
4343import { ts , withRoles , withTags } from "api/components/index.js" ;
4444import { MAX_METADATA_KEYS , metadataSchema } from "common/types/events.js" ;
4545
46+ const createProjectionParams = ( includeMetadata : boolean = false ) => {
47+ // Object mapping attribute names to their expression aliases
48+ const attributeMapping = {
49+ title : "#title" ,
50+ description : "#description" ,
51+ start : "#startTime" , // Reserved keyword
52+ end : "#endTime" , // Potential reserved keyword
53+ location : "#location" ,
54+ locationLink : "#locationLink" ,
55+ host : "#host" ,
56+ featured : "#featured" ,
57+ id : "#id" ,
58+ ...( includeMetadata ? { metadata : "#metadata" } : { } ) ,
59+ } ;
60+
61+ // Create expression attribute names object for DynamoDB
62+ const expressionAttributeNames = Object . entries ( attributeMapping ) . reduce (
63+ ( acc , [ attrName , exprName ] ) => {
64+ acc [ exprName ] = attrName ;
65+ return acc ;
66+ } ,
67+ { } as { [ key : string ] : string } ,
68+ ) ;
69+
70+ // Create projection expression from the values of attributeMapping
71+ const projectionExpression = Object . values ( attributeMapping ) . join ( "," ) ;
72+
73+ return {
74+ attributeMapping,
75+ expressionAttributeNames,
76+ projectionExpression,
77+ // Return function to destructure results if needed
78+ getAttributes : < T > ( item : any ) : T => item as T ,
79+ } ;
80+ } ;
81+
4682const repeatOptions = [ "weekly" , "biweekly" ] as const ;
83+ const zodIncludeMetadata = z . coerce
84+ . boolean ( )
85+ . default ( false )
86+ . optional ( )
87+ . openapi ( {
88+ description : "If true, metadata for each event entry." ,
89+ } ) ;
4790export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${ EVENT_CACHED_DURATION } , stale-while-revalidate=420, stale-if-error=3600` ;
4891export type EventRepeatOptions = ( typeof repeatOptions ) [ number ] ;
4992
@@ -96,11 +139,11 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
96139 {
97140 schema : withTags ( [ "Events" ] , {
98141 querystring : z . object ( {
99- upcomingOnly : z . coerce . boolean ( ) . optional ( ) . openapi ( {
142+ upcomingOnly : z . coerce . boolean ( ) . default ( false ) . optional ( ) . openapi ( {
100143 description :
101144 "If true, only get events which end after the current time." ,
102145 } ) ,
103- featuredOnly : z . coerce . boolean ( ) . optional ( ) . openapi ( {
146+ featuredOnly : z . coerce . boolean ( ) . default ( false ) . optional ( ) . openapi ( {
104147 description :
105148 "If true, only get events which are marked as featured." ,
106149 } ) ,
@@ -109,6 +152,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
109152 . optional ( )
110153 . openapi ( { description : "Event host filter." } ) ,
111154 ts,
155+ includeMetadata : zodIncludeMetadata ,
112156 } ) ,
113157 summary : "Retrieve calendar events with applied filters." ,
114158 // response: { 200: getEventsSchema },
@@ -117,9 +161,10 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
117161 async ( request , reply ) => {
118162 const upcomingOnly = request . query ?. upcomingOnly || false ;
119163 const featuredOnly = request . query ?. featuredOnly || false ;
164+ const includeMetadata = request . query . includeMetadata || true ;
120165 const host = request . query ?. host ;
121166 const ts = request . query ?. ts ; // we only use this to disable cache control
122-
167+ const projection = createProjectionParams ( includeMetadata ) ;
123168 try {
124169 const ifNoneMatch = request . headers [ "if-none-match" ] ;
125170 if ( ifNoneMatch ) {
@@ -151,10 +196,14 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
151196 } ,
152197 KeyConditionExpression : "host = :host" ,
153198 IndexName : "HostIndex" ,
199+ ProjectionExpression : projection . projectionExpression ,
200+ ExpressionAttributeNames : projection . expressionAttributeNames ,
154201 } ) ;
155202 } else {
156203 command = new ScanCommand ( {
157204 TableName : genericConfig . EventsDynamoTableName ,
205+ ProjectionExpression : projection . projectionExpression ,
206+ ExpressionAttributeNames : projection . expressionAttributeNames ,
158207 } ) ;
159208 }
160209 if ( ! ifNoneMatch ) {
@@ -446,6 +495,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
446495 } ) ,
447496 querystring : z . object ( {
448497 ts,
498+ includeMetadata : zodIncludeMetadata ,
449499 } ) ,
450500 summary : "Retrieve a calendar event." ,
451501 // response: { 200: getEventSchema },
@@ -454,6 +504,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
454504 async ( request , reply ) => {
455505 const id = request . params . id ;
456506 const ts = request . query ?. ts ;
507+ const includeMetadata = request . query ?. includeMetadata || false ;
457508
458509 try {
459510 // Check If-None-Match header
@@ -477,11 +528,13 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
477528
478529 reply . header ( "etag" , etag ) ;
479530 }
480-
531+ const projection = createProjectionParams ( includeMetadata ) ;
481532 const response = await fastify . dynamoClient . send (
482533 new GetItemCommand ( {
483534 TableName : genericConfig . EventsDynamoTableName ,
484535 Key : marshall ( { id } ) ,
536+ ProjectionExpression : projection . projectionExpression ,
537+ ExpressionAttributeNames : projection . expressionAttributeNames ,
485538 } ) ,
486539 ) ;
487540 const item = response . Item ? unmarshall ( response . Item ) : null ;
@@ -507,6 +560,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
507560 if ( e instanceof BaseError ) {
508561 throw e ;
509562 }
563+ fastify . log . error ( e ) ;
510564 throw new DatabaseFetchError ( {
511565 message : "Failed to get event from Dynamo table." ,
512566 } ) ;
0 commit comments