Skip to content

Commit d5ff9c6

Browse files
committed
add includeMetadata query parameter
1 parent 1b078b8 commit d5ff9c6

File tree

1 file changed

+58
-4
lines changed

1 file changed

+58
-4
lines changed

src/api/routes/events.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,50 @@ import {
4343
import { ts, withRoles, withTags } from "api/components/index.js";
4444
import { 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+
4682
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+
});
4790
export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${EVENT_CACHED_DURATION}, stale-while-revalidate=420, stale-if-error=3600`;
4891
export 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

Comments
 (0)