@@ -43,7 +43,50 @@ import {
43
43
import { ts , withRoles , withTags } from "api/components/index.js" ;
44
44
import { MAX_METADATA_KEYS , metadataSchema } from "common/types/events.js" ;
45
45
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
+
46
82
const 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
+ } ) ;
47
90
export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${ EVENT_CACHED_DURATION } , stale-while-revalidate=420, stale-if-error=3600` ;
48
91
export type EventRepeatOptions = ( typeof repeatOptions ) [ number ] ;
49
92
@@ -96,11 +139,11 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
96
139
{
97
140
schema : withTags ( [ "Events" ] , {
98
141
querystring : z . object ( {
99
- upcomingOnly : z . coerce . boolean ( ) . optional ( ) . openapi ( {
142
+ upcomingOnly : z . coerce . boolean ( ) . default ( false ) . optional ( ) . openapi ( {
100
143
description :
101
144
"If true, only get events which end after the current time." ,
102
145
} ) ,
103
- featuredOnly : z . coerce . boolean ( ) . optional ( ) . openapi ( {
146
+ featuredOnly : z . coerce . boolean ( ) . default ( false ) . optional ( ) . openapi ( {
104
147
description :
105
148
"If true, only get events which are marked as featured." ,
106
149
} ) ,
@@ -109,6 +152,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
109
152
. optional ( )
110
153
. openapi ( { description : "Event host filter." } ) ,
111
154
ts,
155
+ includeMetadata : zodIncludeMetadata ,
112
156
} ) ,
113
157
summary : "Retrieve calendar events with applied filters." ,
114
158
// response: { 200: getEventsSchema },
@@ -117,9 +161,10 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
117
161
async ( request , reply ) => {
118
162
const upcomingOnly = request . query ?. upcomingOnly || false ;
119
163
const featuredOnly = request . query ?. featuredOnly || false ;
164
+ const includeMetadata = request . query . includeMetadata || true ;
120
165
const host = request . query ?. host ;
121
166
const ts = request . query ?. ts ; // we only use this to disable cache control
122
-
167
+ const projection = createProjectionParams ( includeMetadata ) ;
123
168
try {
124
169
const ifNoneMatch = request . headers [ "if-none-match" ] ;
125
170
if ( ifNoneMatch ) {
@@ -151,10 +196,14 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
151
196
} ,
152
197
KeyConditionExpression : "host = :host" ,
153
198
IndexName : "HostIndex" ,
199
+ ProjectionExpression : projection . projectionExpression ,
200
+ ExpressionAttributeNames : projection . expressionAttributeNames ,
154
201
} ) ;
155
202
} else {
156
203
command = new ScanCommand ( {
157
204
TableName : genericConfig . EventsDynamoTableName ,
205
+ ProjectionExpression : projection . projectionExpression ,
206
+ ExpressionAttributeNames : projection . expressionAttributeNames ,
158
207
} ) ;
159
208
}
160
209
if ( ! ifNoneMatch ) {
@@ -446,6 +495,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
446
495
} ) ,
447
496
querystring : z . object ( {
448
497
ts,
498
+ includeMetadata : zodIncludeMetadata ,
449
499
} ) ,
450
500
summary : "Retrieve a calendar event." ,
451
501
// response: { 200: getEventSchema },
@@ -454,6 +504,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
454
504
async ( request , reply ) => {
455
505
const id = request . params . id ;
456
506
const ts = request . query ?. ts ;
507
+ const includeMetadata = request . query ?. includeMetadata || false ;
457
508
458
509
try {
459
510
// Check If-None-Match header
@@ -477,11 +528,13 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
477
528
478
529
reply . header ( "etag" , etag ) ;
479
530
}
480
-
531
+ const projection = createProjectionParams ( includeMetadata ) ;
481
532
const response = await fastify . dynamoClient . send (
482
533
new GetItemCommand ( {
483
534
TableName : genericConfig . EventsDynamoTableName ,
484
535
Key : marshall ( { id } ) ,
536
+ ProjectionExpression : projection . projectionExpression ,
537
+ ExpressionAttributeNames : projection . expressionAttributeNames ,
485
538
} ) ,
486
539
) ;
487
540
const item = response . Item ? unmarshall ( response . Item ) : null ;
@@ -507,6 +560,7 @@ const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
507
560
if ( e instanceof BaseError ) {
508
561
throw e ;
509
562
}
563
+ fastify . log . error ( e ) ;
510
564
throw new DatabaseFetchError ( {
511
565
message : "Failed to get event from Dynamo table." ,
512
566
} ) ;
0 commit comments