Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
01c3a8d
Add native zod schemas and documntation for 3 modules
devksingh4 Apr 21, 2025
4f2b5ec
add more typing
devksingh4 Apr 21, 2025
e26565a
cache the documentation route more aggressively
devksingh4 Apr 21, 2025
9a8022a
fix unit tests
devksingh4 Apr 21, 2025
e8735c6
fix UI tests
devksingh4 Apr 21, 2025
2042270
add zod-oai banner
devksingh4 Apr 21, 2025
19dba64
Revert "add zod-oai banner"
devksingh4 Apr 21, 2025
c34a945
add import
devksingh4 Apr 21, 2025
e2d00d4
add import to build file
devksingh4 Apr 21, 2025
7c3fb36
install zod-openapi dedicated in the package
devksingh4 Apr 21, 2025
61a4fc0
make zod-openapi external
devksingh4 Apr 21, 2025
4af92cb
copy swagger assets
devksingh4 Apr 21, 2025
059dae2
disable tree shaking
devksingh4 Apr 21, 2025
b7bb4e2
copy addl dir
devksingh4 Apr 21, 2025
5f4455d
trigger deploy
devksingh4 Apr 21, 2025
af5dc6b
mark swagger as external in esbuild
devksingh4 Apr 21, 2025
32c213c
fix package.json
devksingh4 Apr 21, 2025
0b575da
try fixing build
devksingh4 Apr 21, 2025
f969739
WORKING VERSION
devksingh4 Apr 21, 2025
b64afc7
remove root-level registration
devksingh4 Apr 21, 2025
efe34e2
allow caching docs UI route
devksingh4 Apr 21, 2025
5a11bfd
add tag descriptions
devksingh4 Apr 21, 2025
989b283
try this new method of doing auth
devksingh4 Apr 21, 2025
69ce787
update packages
devksingh4 Apr 21, 2025
1db50d1
fix unit tests
devksingh4 Apr 21, 2025
7f98b45
fix live test path
devksingh4 Apr 21, 2025
00f9b84
change build process
devksingh4 Apr 21, 2025
1387443
fix build
devksingh4 Apr 21, 2025
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
19 changes: 19 additions & 0 deletions src/api/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FastifySchema } from "fastify";

Check warning on line 1 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifySchema' is defined but never used. Allowed unused vars must match /^_/u
import { z } from "zod";

export const ts = z.coerce
.number()
.min(0)
.optional()
.openapi({ description: "Staleness bound", example: 0 });
export const groupId = z.string().min(1).openapi({
description: "Entra ID Group ID",
example: "d8cbb7c9-2f6d-4b7e-8ba6-b54f8892003b",
});

export function withTags<T>(tags: string[], schema: T) {
return {

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/auth.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/discordEvent.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/entraGroupManagement.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/entraInviteUser.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/eventPost.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/events.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/health.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/ical.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/linkry.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31

Check failure on line 15 in src/api/components/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

tests/unit/membership.test.ts

TypeError: z.coerce.number(...).min(...).optional(...).openapi is not a function ❯ src/api/components/index.ts:15:3 ❯ src/api/routes/protected.ts:2:31
tags,
...schema,
};
}
9 changes: 1 addition & 8 deletions src/api/functions/auditLog.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
import { genericConfig } from "common/config.js";
import { Modules } from "common/modules.js";

Check warning on line 4 in src/api/functions/auditLog.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'Modules' is defined but never used. Allowed unused vars must match /^_/u

export type AuditLogEntry = {
module: Modules;
actor: string;
target: string;
requestId?: string;
message: string;
};
import { AuditLogEntry } from "common/types/logs.js";

type AuditLogParams = {
dynamoClient?: DynamoDBClient;
Expand Down
34 changes: 33 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
import path from "path"; // eslint-disable-line import/no-nodejs-modules
import roomRequestRoutes from "./routes/roomRequests.js";
import logsPlugin from "./routes/logs.js";
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUI from "@fastify/swagger-ui";
import {
type FastifyZodOpenApiSchema,

Check warning on line 35 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifyZodOpenApiSchema' is defined but never used. Allowed unused vars must match /^_/u
type FastifyZodOpenApiTypeProvider,

Check warning on line 36 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifyZodOpenApiTypeProvider' is defined but never used. Allowed unused vars must match /^_/u
fastifyZodOpenApiPlugin,
fastifyZodOpenApiTransform,
fastifyZodOpenApiTransformObject,
serializerCompiler,

Check warning on line 40 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'serializerCompiler' is defined but never used. Allowed unused vars must match /^_/u
validatorCompiler,

Check warning on line 41 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'validatorCompiler' is defined but never used. Allowed unused vars must match /^_/u
} from "fastify-zod-openapi";
import { ZodOpenApiVersion } from "zod-openapi";
import { withTags } from "./components/index.js";

dotenv.config();

Expand Down Expand Up @@ -85,6 +98,21 @@
await app.register(fastifyZodValidationPlugin);
await app.register(FastifyAuthProvider);
await app.register(errorHandlerPlugin);
await app.register(fastifyZodOpenApiPlugin);
await app.register(fastifySwagger, {
openapi: {
info: {
title: "ACM @ UIUC Core API",
version: "1.0.0",
},
openapi: "3.0.3" satisfies ZodOpenApiVersion, // If this is not specified, it will default to 3.1.0
},
transform: fastifyZodOpenApiTransform,
transformObject: fastifyZodOpenApiTransformObject,
});
await app.register(fastifySwaggerUI, {
routePrefix: "/api/documentation",
});
await app.register(fastifyStatic, {
root: path.join(__dirname, "public"),
prefix: "/",
Expand Down Expand Up @@ -122,7 +150,11 @@
);
done();
});
app.get("/api/v1/healthz", (_, reply) => reply.send({ message: "UP" }));
app.get(
"/api/v1/healthz",
{ schema: withTags(["Generic"], {}) },
(_, reply) => reply.send({ message: "UP" }),
);
await app.register(
async (api, _options) => {
api.register(protectedRoute, { prefix: "/protected" });
Expand Down
4 changes: 4 additions & 0 deletions src/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"@fastify/caching": "^9.0.1",
"@fastify/cors": "^10.0.1",
"@fastify/static": "^8.1.1",
"@fastify/swagger": "^9.5.0",
"@fastify/swagger-ui": "^5.2.2",
"@middy/core": "^6.0.0",
"@middy/event-normalizer": "^6.0.0",
"@middy/sqs-partial-batch-failure": "^6.0.0",
Expand All @@ -40,6 +42,7 @@
"fastify": "^5.1.0",
"fastify-plugin": "^4.5.1",
"fastify-raw-body": "^5.0.0",
"fastify-zod-openapi": "^4.1.1",
"ical-generator": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
Expand All @@ -53,6 +56,7 @@
"stripe": "^17.6.0",
"uuid": "^11.0.5",
"zod": "^3.23.8",
"zod-openapi": "^4.2.4",
"zod-to-json-schema": "^3.23.2",
"zod-validation-error": "^3.3.1"
},
Expand Down
144 changes: 75 additions & 69 deletions src/api/routes/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import "zod-openapi/extend";
import { FastifyPluginAsync, FastifyRequest } from "fastify";

Check warning on line 2 in src/api/routes/events.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifyRequest' is defined but never used. Allowed unused vars must match /^_/u
import { AppRoles } from "../../common/roles.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

Check warning on line 5 in src/api/routes/events.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'zodToJsonSchema' is defined but never used. Allowed unused vars must match /^_/u
import { OrganizationList } from "../../common/orgs.js";
import {
DeleteItemCommand,
Expand Down Expand Up @@ -32,6 +33,14 @@
} from "api/functions/cache.js";
import { createAuditLogEntry } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import {
FastifyPluginAsyncZodOpenApi,
FastifyZodOpenApiSchema,
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { ts, withTags } from "api/components/index.js";

const repeatOptions = ["weekly", "biweekly"] as const;
export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${EVENT_CACHED_DURATION}, stale-while-revalidate=420, stale-if-error=3600`;
Expand All @@ -54,77 +63,57 @@
repeatEnds: z.string().optional(),
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const postRequestSchema = requestSchema.refine(
(data) => (data.repeatEnds ? data.repeats !== undefined : true),
{
message: "repeats is required when repeatEnds is defined",
},
);

export type EventPostRequest = z.infer<typeof postRequestSchema>;
type EventGetRequest = {
Params: { id: string };
Querystring: { ts?: number };
Body: undefined;
};

type EventDeleteRequest = {
Params: { id: string };
Querystring: undefined;
Body: undefined;
};

const responseJsonSchema = zodToJsonSchema(
z.object({
id: z.string(),
resource: z.string(),
}),
);

// GET
const getEventSchema = requestSchema.extend({
id: z.string(),
});

export type EventGetResponse = z.infer<typeof getEventSchema>;
const getEventJsonSchema = zodToJsonSchema(getEventSchema);

const getEventsSchema = z.array(getEventSchema);
export type EventsGetResponse = z.infer<typeof getEventsSchema>;
type EventsGetRequest = {
Body: undefined;
Querystring?: {
upcomingOnly?: boolean;
featuredOnly?: boolean;
host?: string;
ts?: number;
};
};

const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
const eventsPlugin: FastifyPluginAsyncZodOpenApi = async (
fastify,
_options,
) => {
fastify.setValidatorCompiler(validatorCompiler);
fastify.setSerializerCompiler(serializerCompiler);
const limitedRoutes: FastifyPluginAsync = async (fastify) => {
fastify.register(rateLimiter, {
limit: 30,
duration: 60,
rateLimitIdentifier: "events",
});
fastify.get<EventsGetRequest>(
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
"/",
{
schema: {
querystring: {
type: "object",
properties: {
upcomingOnly: { type: "boolean" },
host: { type: "string" },
ts: { type: "number" },
},
},
schema: withTags(["Events"], {
querystring: z.object({
upcomingOnly: z.coerce.boolean().optional().openapi({
description:
"If true, only get events which end after the current time.",
}),
featuredOnly: z.coerce.boolean().optional().openapi({
description:
"If true, only get events which are marked as featured.",
}),
host: z
.enum(OrganizationList as [string, ...string[]])
.optional()
.openapi({ description: "Event host filter." }),
ts,
}),
response: { 200: getEventsSchema },
},
}),
},
async (request: FastifyRequest<EventsGetRequest>, reply) => {
async (request, reply) => {
const upcomingOnly = request.query?.upcomingOnly || false;
const featuredOnly = request.query?.featuredOnly || false;
const host = request.query?.host;
Expand Down Expand Up @@ -230,15 +219,18 @@
);
};

fastify.post<{ Body: EventPostRequest }>(
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
"/:id?",
{
schema: {
response: { 201: responseJsonSchema },
},
preValidation: async (request, reply) => {
await fastify.zodValidateBody(request, reply, postRequestSchema);
},
schema: withTags(["Events"], {
response: {
201: z.object({
id: z.string(),
resource: z.string(),
}),
},
body: postRequestSchema,
}) satisfies FastifyZodOpenApiSchema,
onRequest: async (request, reply) => {
await fastify.authorize(request, reply, [AppRoles.EVENTS_MANAGER]);
},
Expand Down Expand Up @@ -361,17 +353,28 @@
}
},
);
fastify.delete<EventDeleteRequest>(
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().delete(
"/:id",
{
schema: {
response: { 201: responseJsonSchema },
},
schema: withTags(["Events"], {
params: z.object({
id: z.string().min(1).openapi({
description: "Event ID to delete.",
example: "6667e095-8b04-4877-b361-f636f459ba42",
}),
}),
response: {
201: z.object({
id: z.string(),
resource: z.string(),
}),
},
}) satisfies FastifyZodOpenApiSchema,
onRequest: async (request, reply) => {
await fastify.authorize(request, reply, [AppRoles.EVENTS_MANAGER]);
},
},
async (request: FastifyRequest<EventDeleteRequest>, reply) => {
async (request, reply) => {
const id = request.params.id;
if (!request.username) {
throw new UnauthenticatedError({ message: "Username not found." });
Expand Down Expand Up @@ -421,20 +424,23 @@
);
},
);
fastify.get<EventGetRequest>(
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
"/:id",
{
schema: {
querystring: {
type: "object",
properties: {
ts: { type: "number" },
},
},
response: { 200: getEventJsonSchema },
},
schema: withTags(["Events"], {
params: z.object({
id: z.string().min(1).openapi({
description: "Event ID to delete.",
example: "6667e095-8b04-4877-b361-f636f459ba42",
}),
}),
querystring: z.object({
ts,
}),
response: { 200: getEventSchema },
}),
},
async (request: FastifyRequest<EventGetRequest>, reply) => {
async (request, reply) => {
const id = request.params.id;
const ts = request.query?.ts;

Expand Down Expand Up @@ -485,7 +491,7 @@
reply.header("etag", etag);
}

return reply.send(item);
return reply.send(item as z.infer<typeof getEventSchema>);
} catch (e) {
if (e instanceof BaseError) {
throw e;
Expand Down
Loading