Skip to content

Commit d8cdeac

Browse files
authored
Merge pull request #2 from acm-uiuc/events-getall
clean up naming scheme and add ID to get all request
2 parents ae0b4cf + f56e256 commit d8cdeac

File tree

5 files changed

+118
-36
lines changed

5 files changed

+118
-36
lines changed

src/config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { AppRoles, RunEnvironment } from "./roles.js";
2+
import { OriginFunction } from "@fastify/cors";
23

3-
type GroupRoleMapping = Record<string, AppRoles[]>;
4-
type AzureRoleMapping = Record<string, AppRoles[]>;
4+
// From @fastify/cors
5+
type ArrayOfValueOrArray<T> = Array<ValueOrArray<T>>;
6+
type OriginType = string | boolean | RegExp;
7+
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
8+
9+
type GroupRoleMapping = Record<string, readonly AppRoles[]>;
10+
type AzureRoleMapping = Record<string, readonly AppRoles[]>;
511

612
export type ConfigType = {
713
GroupRoleMapping: GroupRoleMapping;
814
AzureRoleMapping: AzureRoleMapping;
9-
ValidCorsOrigins: (string | RegExp)[];
15+
ValidCorsOrigins: ValueOrArray<OriginType> | OriginFunction;
1016
AadValidClientId: string;
1117
};
1218

@@ -60,6 +66,6 @@ const environmentConfig: EnvironmentConfigType = {
6066
],
6167
AadValidClientId: "5e08cf0f-53bb-4e09-9df2-e9bdc3467296",
6268
},
63-
} as const;
69+
};
6470

6571
export { genericConfig, environmentConfig };

src/routes/events.ts

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from "zod";
44
import { zodToJsonSchema } from "zod-to-json-schema";
55
import { OrganizationList } from "../orgs.js";
66
import {
7+
DeleteItemCommand,
78
DynamoDBClient,
89
PutItemCommand,
910
ScanCommand,
@@ -18,7 +19,7 @@ import moment from "moment-timezone";
1819

1920
const repeatOptions = ["weekly", "biweekly"] as const;
2021

21-
const baseBodySchema = z.object({
22+
const baseSchema = z.object({
2223
title: z.string().min(1),
2324
description: z.string().min(1),
2425
start: z.string(),
@@ -30,16 +31,20 @@ const baseBodySchema = z.object({
3031
paidEventId: z.optional(z.string().min(1)),
3132
});
3233

33-
const requestBodySchema = baseBodySchema
34-
.extend({
35-
repeats: z.optional(z.enum(repeatOptions)),
36-
repeatEnds: z.string().optional(),
37-
})
38-
.refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), {
34+
const requestSchema = baseSchema.extend({
35+
repeats: z.optional(z.enum(repeatOptions)),
36+
repeatEnds: z.string().optional(),
37+
});
38+
39+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
40+
const postRequestSchema = requestSchema.refine(
41+
(data) => (data.repeatEnds ? data.repeats !== undefined : true),
42+
{
3943
message: "repeats is required when repeatEnds is defined",
40-
});
44+
},
45+
);
4146

42-
type EventPostRequest = z.infer<typeof requestBodySchema>;
47+
type EventPostRequest = z.infer<typeof postRequestSchema>;
4348

4449
const responseJsonSchema = zodToJsonSchema(
4550
z.object({
@@ -49,9 +54,15 @@ const responseJsonSchema = zodToJsonSchema(
4954
);
5055

5156
// GET
52-
const getResponseBodySchema = z.array(requestBodySchema);
53-
const getResponseJsonSchema = zodToJsonSchema(getResponseBodySchema);
54-
export type EventGetResponse = z.infer<typeof getResponseBodySchema>;
57+
const getEventSchema = requestSchema.extend({
58+
id: z.string(),
59+
});
60+
61+
export type EventGetResponse = z.infer<typeof getEventSchema>;
62+
const getEventJsonSchema = zodToJsonSchema(getEventSchema);
63+
64+
const getEventsSchema = z.array(getEventSchema);
65+
export type EventsGetResponse = z.infer<typeof getEventsSchema>;
5566
type EventsGetQueryParams = { upcomingOnly?: boolean };
5667

5768
const dynamoClient = new DynamoDBClient({
@@ -66,7 +77,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
6677
response: { 200: responseJsonSchema },
6778
},
6879
preValidation: async (request, reply) => {
69-
await fastify.zodValidateBody(request, reply, requestBodySchema);
80+
await fastify.zodValidateBody(request, reply, postRequestSchema);
7081
},
7182
onRequest: async (request, reply) => {
7283
await fastify.authorize(request, reply, [AppRoles.MANAGER]);
@@ -101,6 +112,84 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
101112
}
102113
},
103114
);
115+
type EventGetRequest = {
116+
Params: { id: string };
117+
Querystring: undefined;
118+
Body: undefined;
119+
};
120+
fastify.get<EventGetRequest>(
121+
"/:id",
122+
{
123+
schema: {
124+
response: { 200: getEventJsonSchema },
125+
},
126+
},
127+
async (request: FastifyRequest<EventGetRequest>, reply) => {
128+
const id = request.params.id;
129+
try {
130+
const response = await dynamoClient.send(
131+
new ScanCommand({
132+
TableName: genericConfig.DynamoTableName,
133+
FilterExpression: "#id = :id",
134+
ExpressionAttributeNames: {
135+
"#id": "id",
136+
},
137+
ExpressionAttributeValues: marshall({ ":id": id }),
138+
}),
139+
);
140+
const items = response.Items?.map((item) => unmarshall(item));
141+
if (items?.length !== 1) {
142+
throw new Error("Event not found");
143+
}
144+
reply.send(items[0]);
145+
} catch (e: unknown) {
146+
if (e instanceof Error) {
147+
request.log.error("Failed to get from DynamoDB: " + e.toString());
148+
}
149+
throw new DatabaseFetchError({
150+
message: "Failed to get event from Dynamo table.",
151+
});
152+
}
153+
},
154+
);
155+
type EventDeleteRequest = {
156+
Params: { id: string };
157+
Querystring: undefined;
158+
Body: undefined;
159+
};
160+
fastify.delete<EventDeleteRequest>(
161+
"/:id",
162+
{
163+
schema: {
164+
response: { 200: responseJsonSchema },
165+
},
166+
onRequest: async (request, reply) => {
167+
await fastify.authorize(request, reply, [AppRoles.MANAGER]);
168+
},
169+
},
170+
async (request: FastifyRequest<EventDeleteRequest>, reply) => {
171+
const id = request.params.id;
172+
try {
173+
await dynamoClient.send(
174+
new DeleteItemCommand({
175+
TableName: genericConfig.DynamoTableName,
176+
Key: marshall({ id }),
177+
}),
178+
);
179+
reply.send({
180+
id,
181+
resource: `/api/v1/event/${id}`,
182+
});
183+
} catch (e: unknown) {
184+
if (e instanceof Error) {
185+
request.log.error("Failed to delete from DynamoDB: " + e.toString());
186+
}
187+
throw new DatabaseInsertError({
188+
message: "Failed to delete event from Dynamo table.",
189+
});
190+
}
191+
},
192+
);
104193
type EventsGetRequest = {
105194
Body: undefined;
106195
Querystring?: EventsGetQueryParams;
@@ -112,7 +201,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
112201
querystring: {
113202
upcomingOnly: { type: "boolean" },
114203
},
115-
response: { 200: getResponseJsonSchema },
204+
response: { 200: getEventsSchema },
116205
},
117206
},
118207
async (request: FastifyRequest<EventsGetRequest>, reply) => {
@@ -123,7 +212,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
123212
);
124213
const items = response.Items?.map((item) => unmarshall(item));
125214
const currentTimeChicago = moment().tz("America/Chicago");
126-
let parsedItems = getResponseBodySchema.parse(items);
215+
let parsedItems = getEventsSchema.parse(items);
127216
if (upcomingOnly) {
128217
parsedItems = parsedItems.filter((item) => {
129218
try {

tests/live/events.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from "vitest";
22
import { InternalServerError } from "../../src/errors/index.js";
3-
import { EventGetResponse } from "../../src/routes/events.js";
3+
import { EventsGetResponse } from "../../src/routes/events.js";
44

55
const appKey = process.env.APPLICATION_KEY;
66
if (!appKey) {
@@ -12,6 +12,6 @@ const baseEndpoint = `https://${appKey}.aws.qa.acmuiuc.org`;
1212
test("getting events", async () => {
1313
const response = await fetch(`${baseEndpoint}/api/v1/events`);
1414
expect(response.status).toBe(200);
15-
const responseJson = (await response.json()) as EventGetResponse;
15+
const responseJson = (await response.json()) as EventsGetResponse;
1616
expect(responseJson.length).greaterThan(0);
1717
});

tests/unit/events.test.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { afterAll, expect, test, beforeEach, vi } from "vitest";
2-
import {
3-
ScanCommand,
4-
DynamoDBClient,
5-
PutItemCommand,
6-
} from "@aws-sdk/client-dynamodb";
2+
import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
73
import { mockClient } from "aws-sdk-client-mock";
84
import init from "../../src/index.js";
95
import { EventGetResponse } from "../../src/routes/events.js";
@@ -12,16 +8,9 @@ import {
128
dynamoTableDataUnmarshalled,
139
dynamoTableDataUnmarshalledUpcomingOnly,
1410
} from "./mockEventData.testdata.js";
15-
import { createJwt } from "./auth.test.js";
16-
import {
17-
GetSecretValueCommand,
18-
SecretsManagerClient,
19-
} from "@aws-sdk/client-secrets-manager";
20-
import { secretJson, secretObject } from "./secret.testdata.js";
21-
import supertest from "supertest";
11+
import { secretObject } from "./secret.testdata.js";
2212

2313
const ddbMock = mockClient(DynamoDBClient);
24-
const smMock = mockClient(SecretsManagerClient);
2514
const jwt_secret = secretObject["jwt_key"];
2615
vi.stubEnv("JwtSigningKey", jwt_secret);
2716

tests/unit/mockEventData.testdata.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,13 @@ const dynamoTableData = [
165165

166166
const dynamoTableDataUnmarshalled = dynamoTableData.map((x: any) => {
167167
const temp = unmarshall(x);
168-
delete temp.id;
169168
delete temp.createdBy;
170169
return temp;
171170
});
172171

173172
const dynamoTableDataUnmarshalledUpcomingOnly = dynamoTableData
174173
.map((x: any) => {
175174
const temp = unmarshall(x);
176-
delete temp.id;
177175
delete temp.createdBy;
178176
return temp;
179177
})

0 commit comments

Comments
 (0)