Skip to content

Commit fa6ddab

Browse files
authored
Add native zod schemas and documentation (#120)
* Add native zod schemas and documntation for 3 modules * add more typing * cache the documentation route more aggressively * fix unit tests * fix UI tests * add zod-oai banner * Revert "add zod-oai banner" This reverts commit 2042270. * add import * add import to build file * install zod-openapi dedicated in the package * make zod-openapi external * copy swagger assets * disable tree shaking * copy addl dir * trigger deploy * mark swagger as external in esbuild * fix package.json * try fixing build * WORKING VERSION * remove root-level registration * allow caching docs UI route * add tag descriptions * try this new method of doing auth * update packages * fix unit tests * fix live test path * change build process * fix build
1 parent b6bb60d commit fa6ddab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1123
-785
lines changed

cloudformation/main.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,22 @@ Resources:
736736
- HEAD
737737
CachePolicyId: !Ref CloudfrontCachePolicy
738738
OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
739+
- PathPattern: "/api/documentation*"
740+
TargetOriginId: ApiGatewayOrigin
741+
ViewerProtocolPolicy: redirect-to-https
742+
AllowedMethods:
743+
- GET
744+
- HEAD
745+
- OPTIONS
746+
- PUT
747+
- POST
748+
- DELETE
749+
- PATCH
750+
CachedMethods:
751+
- GET
752+
- HEAD
753+
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
754+
OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
739755
- PathPattern: "/api/*"
740756
TargetOriginId: ApiGatewayOrigin
741757
ViewerProtocolPolicy: redirect-to-https

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"concurrently": "^9.1.2",
4545
"cross-env": "^7.0.3",
4646
"esbuild": "^0.23.0",
47+
"esbuild-plugin-copy": "^2.1.1",
4748
"eslint": "^8.57.0",
4849
"eslint-config-airbnb": "^19.0.4",
4950
"eslint-config-airbnb-typescript": "^18.0.0",

src/api/build.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import esbuild from "esbuild";
22
import { resolve } from "path";
3+
import { copy } from 'esbuild-plugin-copy'
34

45

56
const commonParams = {
@@ -15,7 +16,7 @@ const commonParams = {
1516
target: "es2022", // Target ES2022
1617
sourcemap: false,
1718
platform: "node",
18-
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"],
19+
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify", "zod", "zod-openapi", "@fastify/swagger", "@fastify/swagger-ui"],
1920
alias: {
2021
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
2122
},
@@ -27,8 +28,26 @@ const commonParams = {
2728
const require = topLevelCreateRequire(import.meta.url);
2829
const __filename = fileURLToPath(import.meta.url);
2930
const __dirname = path.dirname(__filename);
31+
import "zod-openapi/extend";
3032
`.trim(),
3133
}, // Banner for compatibility with CommonJS
34+
plugins: [
35+
copy({
36+
resolveFrom: 'cwd',
37+
assets: {
38+
from: ['../../node_modules/@fastify/swagger-ui/static/*'],
39+
to: ['../../dist/lambda/static'],
40+
},
41+
}),
42+
copy({
43+
resolveFrom: 'cwd',
44+
assets: {
45+
from: ['./public/*'],
46+
to: ['../../dist/lambda/public'],
47+
},
48+
}),
49+
],
50+
inject: [resolve(process.cwd(), "./zod-openapi-patch.js")],
3251
}
3352
esbuild
3453
.build({

src/api/components/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { AppRoles } from "common/roles.js";
2+
import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
3+
import { z } from "zod";
4+
5+
export const ts = z.coerce
6+
.number()
7+
.min(0)
8+
.optional()
9+
.openapi({ description: "Staleness bound", example: 0 });
10+
export const groupId = z.string().min(1).openapi({
11+
description: "Entra ID Group ID",
12+
example: "d8cbb7c9-2f6d-4b7e-8ba6-b54f8892003b",
13+
});
14+
15+
export function withTags<T extends FastifyZodOpenApiSchema>(
16+
tags: string[],
17+
schema: T,
18+
) {
19+
return {
20+
tags,
21+
...schema,
22+
};
23+
}
24+
25+
type RoleSchema = {
26+
"x-required-roles": AppRoles[];
27+
description: string;
28+
};
29+
30+
export function withRoles<T extends FastifyZodOpenApiSchema>(
31+
roles: AppRoles[],
32+
schema: T,
33+
): T & RoleSchema {
34+
return {
35+
"x-required-roles": roles,
36+
description: `Requires one of the following roles: ${roles.join(", ")}.${schema.description ? "\n\n" + schema.description : ""}`,
37+
...schema,
38+
};
39+
}

src/api/esbuild.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const buildOptions = {
3030
const require = topLevelCreateRequire(import.meta.url);
3131
const __filename = fileURLToPath(import.meta.url);
3232
const __dirname = path.dirname(__filename);
33+
import "zod-openapi/extend";
3334
`.trim(),
3435
}, // Banner for compatibility with CommonJS
3536
plugins: [copyStaticFiles({

src/api/functions/auditLog.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
22
import { marshall } from "@aws-sdk/util-dynamodb";
33
import { genericConfig } from "common/config.js";
44
import { Modules } from "common/modules.js";
5-
6-
export type AuditLogEntry = {
7-
module: Modules;
8-
actor: string;
9-
target: string;
10-
requestId?: string;
11-
message: string;
12-
};
5+
import { AuditLogEntry } from "common/types/logs.js";
136

147
type AuditLogParams = {
158
dynamoClient?: DynamoDBClient;

src/api/index.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto"]}] */
2+
import "zod-openapi/extend";
23
import { randomUUID } from "crypto";
34
import fastify, { FastifyInstance } from "fastify";
45
import FastifyAuthProvider from "@fastify/auth";
@@ -10,9 +11,9 @@ import { RunEnvironment, runEnvironments } from "../common/roles.js";
1011
import { InternalServerError } from "../common/errors/index.js";
1112
import eventsPlugin from "./routes/events.js";
1213
import cors from "@fastify/cors";
13-
import fastifyZodValidationPlugin from "./plugins/validate.js";
1414
import { environmentConfig, genericConfig } from "../common/config.js";
1515
import organizationsPlugin from "./routes/organizations.js";
16+
import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js";
1617
import icalPlugin from "./routes/ics.js";
1718
import vendingPlugin from "./routes/vending.js";
1819
import * as dotenv from "dotenv";
@@ -29,6 +30,17 @@ import membershipPlugin from "./routes/membership.js";
2930
import path from "path"; // eslint-disable-line import/no-nodejs-modules
3031
import roomRequestRoutes from "./routes/roomRequests.js";
3132
import logsPlugin from "./routes/logs.js";
33+
import fastifySwagger from "@fastify/swagger";
34+
import fastifySwaggerUI from "@fastify/swagger-ui";
35+
import {
36+
fastifyZodOpenApiPlugin,
37+
fastifyZodOpenApiTransform,
38+
fastifyZodOpenApiTransformObject,
39+
serializerCompiler,
40+
validatorCompiler,
41+
} from "fastify-zod-openapi";
42+
import { ZodOpenApiVersion } from "zod-openapi";
43+
import { withTags } from "./components/index.js";
3244

3345
dotenv.config();
3446

@@ -81,10 +93,68 @@ async function init(prettyPrint: boolean = false) {
8193
return event.requestContext.requestId;
8294
},
8395
});
96+
app.setValidatorCompiler(validatorCompiler);
97+
app.setSerializerCompiler(serializerCompiler);
98+
await app.register(authorizeFromSchemaPlugin);
8499
await app.register(fastifyAuthPlugin);
85-
await app.register(fastifyZodValidationPlugin);
86100
await app.register(FastifyAuthProvider);
87101
await app.register(errorHandlerPlugin);
102+
await app.register(fastifyZodOpenApiPlugin);
103+
await app.register(fastifySwagger, {
104+
openapi: {
105+
info: {
106+
title: "ACM @ UIUC Core API",
107+
description: "ACM @ UIUC Core Management Platform",
108+
version: "1.0.0",
109+
},
110+
servers: [
111+
app.runEnvironment === "prod"
112+
? {
113+
url: "https://core.acm.illinois.edu",
114+
description: "Production API server",
115+
}
116+
: {
117+
url: "https://core.aws.qa.acmuiuc.org",
118+
description: "QA API server",
119+
},
120+
],
121+
tags: [
122+
{
123+
name: "Events",
124+
description:
125+
"Retrieve ACM @ UIUC-wide and organization-specific calendars and event metadata.",
126+
},
127+
{
128+
name: "Generic",
129+
description: "Retrieve metadata about a user or ACM @ UIUC .",
130+
},
131+
{
132+
name: "iCalendar Integration",
133+
description:
134+
"Retrieve Events calendars in iCalendar format (for integration with external calendar clients).",
135+
},
136+
{
137+
name: "IAM",
138+
description: "Identity and Access Management for internal services.",
139+
},
140+
{ name: "Linkry", description: "Link Shortener." },
141+
{
142+
name: "Logging",
143+
description: "View audit logs for various services.",
144+
},
145+
{
146+
name: "Membership",
147+
description: "Purchasing or checking ACM @ UIUC membership.",
148+
},
149+
],
150+
openapi: "3.0.3" satisfies ZodOpenApiVersion, // If this is not specified, it will default to 3.1.0
151+
},
152+
transform: fastifyZodOpenApiTransform,
153+
transformObject: fastifyZodOpenApiTransformObject,
154+
});
155+
await app.register(fastifySwaggerUI, {
156+
routePrefix: "/api/documentation",
157+
});
88158
await app.register(fastifyStatic, {
89159
root: path.join(__dirname, "public"),
90160
prefix: "/",
@@ -122,7 +192,11 @@ async function init(prettyPrint: boolean = false) {
122192
);
123193
done();
124194
});
125-
app.get("/api/v1/healthz", (_, reply) => reply.send({ message: "UP" }));
195+
app.get(
196+
"/api/v1/healthz",
197+
{ schema: withTags(["Generic"], {}) },
198+
(_, reply) => reply.send({ message: "UP" }),
199+
);
126200
await app.register(
127201
async (api, _options) => {
128202
api.register(protectedRoute, { prefix: "/protected" });

src/api/lambda.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable */
22

3+
import "zod-openapi/extend";
34
import awsLambdaFastify from "@fastify/aws-lambda";
45
import init from "./index.js";
56

src/api/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"@fastify/caching": "^9.0.1",
3030
"@fastify/cors": "^10.0.1",
3131
"@fastify/static": "^8.1.1",
32+
"@fastify/swagger": "^9.5.0",
33+
"@fastify/swagger-ui": "^5.2.2",
3234
"@middy/core": "^6.0.0",
3335
"@middy/event-normalizer": "^6.0.0",
3436
"@middy/sqs-partial-batch-failure": "^6.0.0",
@@ -37,9 +39,10 @@
3739
"discord.js": "^14.15.3",
3840
"dotenv": "^16.4.5",
3941
"esbuild": "^0.24.2",
40-
"fastify": "^5.1.0",
42+
"fastify": "^5.3.2",
4143
"fastify-plugin": "^4.5.1",
4244
"fastify-raw-body": "^5.0.0",
45+
"fastify-zod-openapi": "^4.1.1",
4346
"ical-generator": "^7.2.0",
4447
"jsonwebtoken": "^9.0.2",
4548
"jwks-rsa": "^3.1.0",
@@ -53,6 +56,7 @@
5356
"stripe": "^17.6.0",
5457
"uuid": "^11.0.5",
5558
"zod": "^3.23.8",
59+
"zod-openapi": "^4.2.4",
5660
"zod-to-json-schema": "^3.23.2",
5761
"zod-validation-error": "^3.3.1"
5862
},

src/api/package.lambda.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
"dependencies": {
1010
"moment-timezone": "^0.5.45",
1111
"passkit-generator": "^3.3.1",
12-
"fastify": "^5.1.0"
12+
"fastify": "^5.3.2",
13+
"@fastify/swagger": "^9.5.0",
14+
"@fastify/swagger-ui": "^5.2.2",
15+
"zod": "^3.23.8",
16+
"zod-openapi": "^4.2.4"
1317
},
1418
"devDependencies": {}
1519
}

0 commit comments

Comments
 (0)