Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion src/api/routes/roomRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { z } from "zod";
import { buildAuditLogTransactPut } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import {
generateProjectionParams,
getDefaultFilteringQuerystring,
nonEmptyCommaSeparatedStringSchema,
} from "common/utils.js";

const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
await fastify.register(rateLimiter, {
Expand Down Expand Up @@ -182,12 +187,19 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
example: "sp25",
}),
}),
querystring: z.object(
getDefaultFilteringQuerystring({
defaultSelect: ["requestId", "title"],
}),
),
}),
),
onRequest: fastify.authorizeFromSchema,
},
async (request, reply) => {
const semesterId = request.params.semesterId;
const { ProjectionExpression, ExpressionAttributeNames } =
generateProjectionParams({ userFields: request.query.select });
if (!request.username) {
throw new InternalServerError({
message: "Could not retrieve username.",
Expand All @@ -198,6 +210,8 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
command = new QueryCommand({
TableName: genericConfig.RoomRequestsTableName,
KeyConditionExpression: "semesterId = :semesterValue",
ProjectionExpression,
ExpressionAttributeNames,
ExpressionAttributeValues: {
":semesterValue": { S: semesterId },
},
Expand All @@ -209,8 +223,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
"semesterId = :semesterValue AND begins_with(#sortKey, :username)",
ExpressionAttributeNames: {
"#sortKey": "userId#requestId",
...ExpressionAttributeNames,
},
ProjectionExpression: "requestId, host, title, semester",
ProjectionExpression,
ExpressionAttributeValues: {
":semesterValue": { S: semesterId },
":username": { S: request.username },
Expand All @@ -224,6 +239,9 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
});
}
const items = response.Items.map((x) => {
if (!request.query.select.includes("status")) {
return unmarshall(x);
}
const item = unmarshall(x) as {
host: string;
title: string;
Expand Down Expand Up @@ -403,20 +421,29 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
example: "sp25",
}),
}),
querystring: z.object(
getDefaultFilteringQuerystring({
defaultSelect: ["requestId", "title"],
}),
),
}),
),
onRequest: fastify.authorizeFromSchema,
},
async (request, reply) => {
const requestId = request.params.requestId;
const semesterId = request.params.semesterId;
const { ProjectionExpression, ExpressionAttributeNames } =
generateProjectionParams({ userFields: request.query.select });
let command;
if (request.userRoles?.has(AppRoles.BYPASS_OBJECT_LEVEL_AUTH)) {
command = new QueryCommand({
TableName: genericConfig.RoomRequestsTableName,
IndexName: "RequestIdIndex",
KeyConditionExpression: "requestId = :requestId",
FilterExpression: "semesterId = :semesterId",
ProjectionExpression,
ExpressionAttributeNames,
ExpressionAttributeValues: {
":requestId": { S: requestId },
":semesterId": { S: semesterId },
Expand All @@ -426,6 +453,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
} else {
command = new QueryCommand({
TableName: genericConfig.RoomRequestsTableName,
ProjectionExpression,
KeyConditionExpression:
"semesterId = :semesterId AND #userIdRequestId = :userRequestId",
ExpressionAttributeValues: {
Expand All @@ -434,6 +462,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
},
ExpressionAttributeNames: {
"#userIdRequestId": "userId#requestId",
...ExpressionAttributeNames,
},
Limit: 1,
});
Expand Down
42 changes: 42 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { z } from "zod";
export function transformCommaSeperatedName(name: string) {
if (name.includes(",")) {
try {
Expand All @@ -12,3 +13,44 @@ export function transformCommaSeperatedName(name: string) {
}
return name;
}

type GenerateProjectionParamsInput = {
userFields?: string[];
}
/**
* Generates DynamoDB projection parameters for select filters, while safely handle reserved keywords.
*/
export const generateProjectionParams = ({ userFields }: GenerateProjectionParamsInput) => {
const attributes = userFields || [];
const expressionAttributeNames: Record<string, string> = {};
const projectionExpression = attributes
.map((attr, index) => {
const placeholder = `#proj${index}`;
expressionAttributeNames[placeholder] = attr;
return placeholder;
})
.join(',');
return {
ProjectionExpression: projectionExpression,
ExpressionAttributeNames: expressionAttributeNames,
};
};


export const nonEmptyCommaSeparatedStringSchema = z
.string({ invalid_type_error: "Filter expression must be a string." })
.min(1, { message: "Filter expression must be at least 1 character long." })
.transform((val) => val.split(',').map(item => item.trim()))
.pipe(z.array(z.string()).nonempty());

type GetDefaultFilteringQuerystringInput = {
defaultSelect: string[];
}
export const getDefaultFilteringQuerystring = ({ defaultSelect }: GetDefaultFilteringQuerystringInput) => {
return {
select: z.optional(nonEmptyCommaSeparatedStringSchema).default(defaultSelect.join(',')).openapi({
description: "Comma-seperated list of attributes to return",
...(defaultSelect.length === 0 ? { default: "<ALL ATTRIBUTES>" } : {}),
})
}
}
12 changes: 10 additions & 2 deletions src/ui/pages/roomRequest/RoomRequestLanding.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RoomRequestFormValues,
RoomRequestGetAllResponse,
RoomRequestPostResponse,
type RoomRequestStatus,
} from "@common/types/roomRequest";

export const ManageRoomRequestsPage: React.FC = () => {
Expand All @@ -29,8 +30,15 @@ export const ManageRoomRequestsPage: React.FC = () => {
const getRoomRequests = async (
semester: string,
): Promise<RoomRequestGetAllResponse> => {
const response = await api.get(`/api/v1/roomRequests/${semester}`);
return response.data;
const response = await api.get<
{
requestId: string;
title: string;
host: string;
status: RoomRequestStatus;
}[]
>(`/api/v1/roomRequests/${semester}?select=requestId,title,host,status`);
return response.data.map((x) => ({ ...x, semester }));
};

useEffect(() => {
Expand Down
Loading