Skip to content

Commit 4f2b5ec

Browse files
committed
add more typing
1 parent 01c3a8d commit 4f2b5ec

File tree

6 files changed

+193
-142
lines changed

6 files changed

+193
-142
lines changed

src/api/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
validatorCompiler,
4242
} from "fastify-zod-openapi";
4343
import { ZodOpenApiVersion } from "zod-openapi";
44+
import { withTags } from "./components/index.js";
4445

4546
dotenv.config();
4647

@@ -149,7 +150,11 @@ async function init(prettyPrint: boolean = false) {
149150
);
150151
done();
151152
});
152-
app.get("/api/v1/healthz", (_, reply) => reply.send({ message: "UP" }));
153+
app.get(
154+
"/api/v1/healthz",
155+
{ schema: withTags(["Generic"], {}) },
156+
(_, reply) => reply.send({ message: "UP" }),
157+
);
153158
await app.register(
154159
async (api, _options) => {
155160
api.register(protectedRoute, { prefix: "/protected" });

src/api/routes/ics.ts

Lines changed: 130 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ import { OrganizationList } from "../../common/orgs.js";
1818
import { CLIENT_HTTP_CACHE_POLICY, EventRepeatOptions } from "./events.js";
1919
import rateLimiter from "api/plugins/rateLimiter.js";
2020
import { getCacheCounter } from "api/functions/cache.js";
21+
import {
22+
FastifyZodOpenApiSchema,
23+
FastifyZodOpenApiTypeProvider,
24+
serializerCompiler,
25+
validatorCompiler,
26+
} from "fastify-zod-openapi";
27+
import { withTags } from "api/components/index.js";
28+
import { z } from "zod";
2129

2230
const repeatingIcalMap: Record<EventRepeatOptions, ICalEventJSONRepeatingData> =
2331
{
@@ -36,128 +44,144 @@ function generateHostName(host: string) {
3644
}
3745

3846
const icalPlugin: FastifyPluginAsync = async (fastify, _options) => {
47+
fastify.setValidatorCompiler(validatorCompiler);
48+
fastify.setSerializerCompiler(serializerCompiler);
3949
fastify.register(rateLimiter, {
4050
limit: OrganizationList.length,
4151
duration: 30,
4252
rateLimitIdentifier: "ical",
4353
});
44-
fastify.get("/:host?", async (request, reply) => {
45-
const host = (request.params as Record<string, string>).host;
46-
let queryParams: QueryCommandInput = {
47-
TableName: genericConfig.EventsDynamoTableName,
48-
};
49-
let response;
50-
const ifNoneMatch = request.headers["if-none-match"];
51-
if (ifNoneMatch) {
52-
const etag = await getCacheCounter(
53-
fastify.dynamoClient,
54-
`events-etag-${host || "all"}`,
55-
);
54+
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
55+
"/:host?",
56+
{
57+
schema: withTags(["iCalendar Integration"], {
58+
params: z.object({
59+
host: z
60+
.optional(z.enum(OrganizationList as [string, ...string[]]))
61+
.openapi({ description: "Host to get calendar for." }),
62+
}),
63+
} satisfies FastifyZodOpenApiSchema),
64+
},
65+
async (request, reply) => {
66+
const host = request.params.host || "ACM";
67+
let queryParams: QueryCommandInput = {
68+
TableName: genericConfig.EventsDynamoTableName,
69+
};
70+
let response;
71+
const ifNoneMatch = request.headers["if-none-match"];
72+
if (ifNoneMatch) {
73+
const etag = await getCacheCounter(
74+
fastify.dynamoClient,
75+
`events-etag-${host || "all"}`,
76+
);
5677

57-
if (
58-
ifNoneMatch === `"${etag.toString()}"` ||
59-
ifNoneMatch === etag.toString()
60-
) {
61-
return reply
62-
.code(304)
63-
.header("ETag", etag)
64-
.header("Cache-Control", CLIENT_HTTP_CACHE_POLICY)
65-
.send();
66-
}
78+
if (
79+
ifNoneMatch === `"${etag.toString()}"` ||
80+
ifNoneMatch === etag.toString()
81+
) {
82+
return reply
83+
.code(304)
84+
.header("ETag", etag)
85+
.header("Cache-Control", CLIENT_HTTP_CACHE_POLICY)
86+
.send();
87+
}
6788

68-
reply.header("etag", etag);
69-
}
70-
if (host) {
71-
if (!OrganizationList.includes(host)) {
72-
throw new ValidationError({
73-
message: `Invalid host parameter "${host}" in path.`,
74-
});
89+
reply.header("etag", etag);
7590
}
76-
queryParams = {
77-
...queryParams,
78-
};
79-
response = await fastify.dynamoClient.send(
80-
new QueryCommand({
91+
if (host) {
92+
if (!OrganizationList.includes(host)) {
93+
throw new ValidationError({
94+
message: `Invalid host parameter "${host}" in path.`,
95+
});
96+
}
97+
queryParams = {
8198
...queryParams,
82-
ExpressionAttributeValues: {
83-
":host": {
84-
S: host,
99+
};
100+
response = await fastify.dynamoClient.send(
101+
new QueryCommand({
102+
...queryParams,
103+
ExpressionAttributeValues: {
104+
":host": {
105+
S: host,
106+
},
85107
},
86-
},
87-
KeyConditionExpression: "host = :host",
88-
IndexName: "HostIndex",
89-
}),
90-
);
91-
} else {
92-
response = await fastify.dynamoClient.send(new ScanCommand(queryParams));
93-
}
94-
const dynamoItems = response.Items
95-
? response.Items.map((x) => unmarshall(x))
96-
: null;
97-
if (!dynamoItems) {
98-
throw new NotFoundError({
99-
endpointName: host ? `/api/v1/ical/${host}` : "/api/v1/ical",
100-
});
101-
}
102-
// generate friendly calendar name
103-
let calendarName =
104-
host && host.includes("ACM")
105-
? `${host} Events`
106-
: `ACM@UIUC - ${host} Events`;
107-
if (host == "ACM") {
108-
calendarName = "ACM@UIUC - Major Events";
109-
}
110-
if (!host) {
111-
calendarName = "ACM@UIUC - All Events";
112-
}
113-
const calendar = ical({ name: calendarName });
114-
calendar.timezone({
115-
name: "America/Chicago",
116-
generator: getVtimezoneComponent,
117-
});
118-
calendar.method(ICalCalendarMethod.PUBLISH);
119-
for (const rawEvent of dynamoItems) {
120-
let event = calendar.createEvent({
121-
start: moment.tz(rawEvent.start, "America/Chicago"),
122-
end: rawEvent.end
123-
? moment.tz(rawEvent.end, "America/Chicago")
124-
: moment.tz(rawEvent.start, "America/Chicago"),
125-
summary: rawEvent.title,
126-
description: rawEvent.locationLink
127-
? `Host: ${rawEvent.host}\nGoogle Maps Link: ${rawEvent.locationLink}\n\n` +
128-
rawEvent.description
129-
: `Host: ${rawEvent.host}\n\n` + rawEvent.description,
130-
timezone: "America/Chicago",
131-
organizer: generateHostName(host),
132-
id: rawEvent.id,
108+
KeyConditionExpression: "host = :host",
109+
IndexName: "HostIndex",
110+
}),
111+
);
112+
} else {
113+
response = await fastify.dynamoClient.send(
114+
new ScanCommand(queryParams),
115+
);
116+
}
117+
const dynamoItems = response.Items
118+
? response.Items.map((x) => unmarshall(x))
119+
: null;
120+
if (!dynamoItems) {
121+
throw new NotFoundError({
122+
endpointName: host ? `/api/v1/ical/${host}` : "/api/v1/ical",
123+
});
124+
}
125+
// generate friendly calendar name
126+
let calendarName =
127+
host && host.includes("ACM")
128+
? `${host} Events`
129+
: `ACM@UIUC - ${host} Events`;
130+
if (host == "ACM") {
131+
calendarName = "ACM@UIUC - Major Events";
132+
}
133+
if (!host) {
134+
calendarName = "ACM@UIUC - All Events";
135+
}
136+
const calendar = ical({ name: calendarName });
137+
calendar.timezone({
138+
name: "America/Chicago",
139+
generator: getVtimezoneComponent,
133140
});
141+
calendar.method(ICalCalendarMethod.PUBLISH);
142+
for (const rawEvent of dynamoItems) {
143+
let event = calendar.createEvent({
144+
start: moment.tz(rawEvent.start, "America/Chicago"),
145+
end: rawEvent.end
146+
? moment.tz(rawEvent.end, "America/Chicago")
147+
: moment.tz(rawEvent.start, "America/Chicago"),
148+
summary: rawEvent.title,
149+
description: rawEvent.locationLink
150+
? `Host: ${rawEvent.host}\nGoogle Maps Link: ${rawEvent.locationLink}\n\n` +
151+
rawEvent.description
152+
: `Host: ${rawEvent.host}\n\n` + rawEvent.description,
153+
timezone: "America/Chicago",
154+
organizer: generateHostName(host),
155+
id: rawEvent.id,
156+
});
134157

135-
if (rawEvent.repeats) {
136-
if (rawEvent.repeatEnds) {
137-
event = event.repeating({
138-
...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions],
139-
until: moment.tz(rawEvent.repeatEnds, "America/Chicago"),
158+
if (rawEvent.repeats) {
159+
if (rawEvent.repeatEnds) {
160+
event = event.repeating({
161+
...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions],
162+
until: moment.tz(rawEvent.repeatEnds, "America/Chicago"),
163+
});
164+
} else {
165+
event.repeating(
166+
repeatingIcalMap[rawEvent.repeats as EventRepeatOptions],
167+
);
168+
}
169+
}
170+
if (rawEvent.location) {
171+
event = event.location({
172+
title: rawEvent.location,
140173
});
141-
} else {
142-
event.repeating(
143-
repeatingIcalMap[rawEvent.repeats as EventRepeatOptions],
144-
);
145174
}
146175
}
147-
if (rawEvent.location) {
148-
event = event.location({
149-
title: rawEvent.location,
150-
});
151-
}
152-
}
153176

154-
reply
155-
.headers({
156-
"Content-Type": "text/calendar; charset=utf-8",
157-
"Content-Disposition": 'attachment; filename="calendar.ics"',
158-
})
159-
.send(calendar.toString());
160-
});
177+
reply
178+
.headers({
179+
"Content-Type": "text/calendar; charset=utf-8",
180+
"Content-Disposition": 'attachment; filename="calendar.ics"',
181+
})
182+
.send(calendar.toString());
183+
},
184+
);
161185
};
162186

163187
export default icalPlugin;

src/api/routes/membership.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => {
227227
};
228228
fastify.post(
229229
"/provision",
230-
{ config: { rawBody: true } },
230+
{ config: { rawBody: true }, schema: { hide: true } },
231231
async (request, reply) => {
232232
let event: Stripe.Event;
233233
if (!request.rawBody) {

src/api/routes/organizations.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FastifyPluginAsync } from "fastify";
22
import { OrganizationList } from "../../common/orgs.js";
33
import fastifyCaching from "@fastify/caching";
44
import rateLimiter from "api/plugins/rateLimiter.js";
5+
import { withTags } from "api/components/index.js";
56

67
const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => {
78
fastify.register(fastifyCaching, {
@@ -14,9 +15,13 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => {
1415
duration: 60,
1516
rateLimitIdentifier: "organizations",
1617
});
17-
fastify.get("/", {}, async (request, reply) => {
18-
reply.send(OrganizationList);
19-
});
18+
fastify.get(
19+
"/",
20+
{ schema: withTags(["Generic"], {}) },
21+
async (request, reply) => {
22+
reply.send(OrganizationList);
23+
},
24+
);
2025
};
2126

2227
export default organizationsPlugin;

src/api/routes/protected.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { FastifyPluginAsync } from "fastify";
22
import rateLimiter from "api/plugins/rateLimiter.js";
3+
import { withTags } from "api/components/index.js";
34

45
const protectedRoute: FastifyPluginAsync = async (fastify, _options) => {
56
await fastify.register(rateLimiter, {
67
limit: 15,
78
duration: 30,
89
rateLimitIdentifier: "protected",
910
});
10-
fastify.get("/", async (request, reply) => {
11-
const roles = await fastify.authorize(request, reply, []);
12-
reply.send({ username: request.username, roles: Array.from(roles) });
13-
});
11+
fastify.get(
12+
"/",
13+
{ schema: withTags(["Generic"], {}) },
14+
async (request, reply) => {
15+
const roles = await fastify.authorize(request, reply, []);
16+
reply.send({ username: request.username, roles: Array.from(roles) });
17+
},
18+
);
1419
};
1520

1621
export default protectedRoute;

0 commit comments

Comments
 (0)