Skip to content

Add native zod schemas and documentation #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 21, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions cloudformation/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,22 @@ Resources:
- HEAD
CachePolicyId: !Ref CloudfrontCachePolicy
OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
- PathPattern: "/api/documentation*"
TargetOriginId: ApiGatewayOrigin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
- OPTIONS
- PUT
- POST
- DELETE
- PATCH
CachedMethods:
- GET
- HEAD
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
- PathPattern: "/api/*"
TargetOriginId: ApiGatewayOrigin
ViewerProtocolPolicy: redirect-to-https
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"esbuild": "^0.23.0",
"esbuild-plugin-copy": "^2.1.1",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
Expand Down
21 changes: 20 additions & 1 deletion src/api/build.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import esbuild from "esbuild";
import { resolve } from "path";
import { copy } from 'esbuild-plugin-copy'


const commonParams = {
Expand All @@ -15,7 +16,7 @@ const commonParams = {
target: "es2022", // Target ES2022
sourcemap: false,
platform: "node",
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"],
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify", "zod", "zod-openapi", "@fastify/swagger", "@fastify/swagger-ui"],
alias: {
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
},
Expand All @@ -27,8 +28,26 @@ const commonParams = {
const require = topLevelCreateRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import "zod-openapi/extend";
`.trim(),
}, // Banner for compatibility with CommonJS
plugins: [
copy({
resolveFrom: 'cwd',
assets: {
from: ['../../node_modules/@fastify/swagger-ui/static/*'],
to: ['../../dist/lambda/static'],
},
}),
copy({
resolveFrom: 'cwd',
assets: {
from: ['./public/*'],
to: ['../../dist/lambda/public'],
},
}),
],
inject: [resolve(process.cwd(), "./zod-openapi-patch.js")],
}
esbuild
.build({
Expand Down
39 changes: 39 additions & 0 deletions src/api/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { AppRoles } from "common/roles.js";
import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
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 extends FastifyZodOpenApiSchema>(
tags: string[],
schema: T,
) {
return {
tags,
...schema,
};
}

type RoleSchema = {
"x-required-roles": AppRoles[];
description: string;
};

export function withRoles<T extends FastifyZodOpenApiSchema>(
roles: AppRoles[],
schema: T,
): T & RoleSchema {
return {
"x-required-roles": roles,
description: `Requires one of the following roles: ${roles.join(", ")}.${schema.description ? "\n\n" + schema.description : ""}`,
...schema,
};
}
1 change: 1 addition & 0 deletions src/api/esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const buildOptions = {
const require = topLevelCreateRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import "zod-openapi/extend";
`.trim(),
}, // Banner for compatibility with CommonJS
plugins: [copyStaticFiles({
Expand Down
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
80 changes: 77 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto"]}] */
import "zod-openapi/extend";
import { randomUUID } from "crypto";
import fastify, { FastifyInstance } from "fastify";
import FastifyAuthProvider from "@fastify/auth";
Expand All @@ -10,9 +11,9 @@ import { RunEnvironment, runEnvironments } from "../common/roles.js";
import { InternalServerError } from "../common/errors/index.js";
import eventsPlugin from "./routes/events.js";
import cors from "@fastify/cors";
import fastifyZodValidationPlugin from "./plugins/validate.js";
import { environmentConfig, genericConfig } from "../common/config.js";
import organizationsPlugin from "./routes/organizations.js";
import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js";
import icalPlugin from "./routes/ics.js";
import vendingPlugin from "./routes/vending.js";
import * as dotenv from "dotenv";
Expand All @@ -29,6 +30,17 @@ import membershipPlugin from "./routes/membership.js";
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 {
fastifyZodOpenApiPlugin,
fastifyZodOpenApiTransform,
fastifyZodOpenApiTransformObject,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { ZodOpenApiVersion } from "zod-openapi";
import { withTags } from "./components/index.js";

dotenv.config();

Expand Down Expand Up @@ -81,10 +93,68 @@ async function init(prettyPrint: boolean = false) {
return event.requestContext.requestId;
},
});
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
await app.register(authorizeFromSchemaPlugin);
await app.register(fastifyAuthPlugin);
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",
description: "ACM @ UIUC Core Management Platform",
version: "1.0.0",
},
servers: [
app.runEnvironment === "prod"
? {
url: "https://core.acm.illinois.edu",
description: "Production API server",
}
: {
url: "https://core.aws.qa.acmuiuc.org",
description: "QA API server",
},
],
tags: [
{
name: "Events",
description:
"Retrieve ACM @ UIUC-wide and organization-specific calendars and event metadata.",
},
{
name: "Generic",
description: "Retrieve metadata about a user or ACM @ UIUC .",
},
{
name: "iCalendar Integration",
description:
"Retrieve Events calendars in iCalendar format (for integration with external calendar clients).",
},
{
name: "IAM",
description: "Identity and Access Management for internal services.",
},
{ name: "Linkry", description: "Link Shortener." },
{
name: "Logging",
description: "View audit logs for various services.",
},
{
name: "Membership",
description: "Purchasing or checking ACM @ UIUC membership.",
},
],
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 +192,11 @@ async function init(prettyPrint: boolean = false) {
);
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
1 change: 1 addition & 0 deletions src/api/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable */

import "zod-openapi/extend";
import awsLambdaFastify from "@fastify/aws-lambda";
import init from "./index.js";

Expand Down
6 changes: 5 additions & 1 deletion 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 @@ -37,9 +39,10 @@
"discord.js": "^14.15.3",
"dotenv": "^16.4.5",
"esbuild": "^0.24.2",
"fastify": "^5.1.0",
"fastify": "^5.3.2",
"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
6 changes: 5 additions & 1 deletion src/api/package.lambda.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"dependencies": {
"moment-timezone": "^0.5.45",
"passkit-generator": "^3.3.1",
"fastify": "^5.1.0"
"fastify": "^5.3.2",
"@fastify/swagger": "^9.5.0",
"@fastify/swagger-ui": "^5.2.2",
"zod": "^3.23.8",
"zod-openapi": "^4.2.4"
},
"devDependencies": {}
}
33 changes: 33 additions & 0 deletions src/api/plugins/authorizeFromSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FastifyPluginAsync } from "fastify";
import { AppRoles } from "common/roles.js";
import { InternalServerError } from "common/errors/index.js";
import fp from "fastify-plugin";

declare module "fastify" {
interface FastifyInstance {
authorizeFromSchema: (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;
}
}

const authorizeFromSchemaPlugin: FastifyPluginAsync = fp(async (fastify) => {
fastify.decorate("authorizeFromSchema", async (request, reply) => {
const schema = request.routeOptions?.schema;

if (!schema || !("x-required-roles" in schema)) {
throw new InternalServerError({
message:
"Server has not specified authentication roles for this route.",
});
}

const roles = (schema as { "x-required-roles": AppRoles[] })[
"x-required-roles"
];
await fastify.authorize(request, reply, roles);
});
});

export default authorizeFromSchemaPlugin;
38 changes: 0 additions & 38 deletions src/api/plugins/validate.ts

This file was deleted.

Loading
Loading