From 1e64bc2f3d3542efba5bf167af996b6982129167 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Wed, 7 Aug 2024 10:20:19 -0400 Subject: [PATCH] allow repeats option --- docs/swagger.yml | 43 +++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- src/index.ts | 6 ++++-- src/plugins/validate.ts | 35 +++++++++++++++++++++++++++++++++ src/routes/events.ts | 18 +++++++++++++---- src/types.d.ts | 5 +++++ yarn.lock | 5 +++++ 7 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 src/plugins/validate.ts diff --git a/docs/swagger.yml b/docs/swagger.yml index 9ed99fb..c91ab57 100644 --- a/docs/swagger.yml +++ b/docs/swagger.yml @@ -34,6 +34,49 @@ paths: summary: Returns the authenticated user's username. operationId: pingAuthenticated + responses: + 200: + description: OK + + x-amazon-apigateway-auth: + type: NONE + + x-amazon-apigateway-integration: + responses: + default: + statusCode: 200 + passthroughBehavior: when_no_match + httpMethod: POST + contentHandling: CONVERT_TO_TEXT + type: aws_proxy + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ApplicationPrefix}-lambda/invocations" + /api/v1/events: + get: + summary: Get all ACM Events + operationId: getAllEvents + + responses: + 200: + description: OK + + x-amazon-apigateway-auth: + type: NONE + + x-amazon-apigateway-integration: + responses: + default: + statusCode: 200 + passthroughBehavior: when_no_match + httpMethod: POST + contentHandling: CONVERT_TO_TEXT + type: aws_proxy + uri: + Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ApplicationPrefix}-lambda/invocations" + post: + summary: Add an ACM event conforming to the Zod schema. + operationId: addEvent + responses: 200: description: OK diff --git a/package.json b/package.json index 958caf3..26e5ab4 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.2" + "zod-to-json-schema": "^3.23.2", + "zod-validation-error": "^3.3.1" } } diff --git a/src/index.ts b/src/index.ts index e86faa9..64f8434 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { RunEnvironment, runEnvironments } from "./roles.js"; import { InternalServerError } from "./errors/index.js"; import eventsPlugin from "./routes/events.js"; import cors from "@fastify/cors"; +import fastifyZodValidationPlugin from "./plugins/validate.js"; const now = () => Date.now(); @@ -27,6 +28,7 @@ async function init() { }, }); await app.register(fastifyAuthPlugin); + await app.register(fastifyZodValidationPlugin); await app.register(FastifyAuthProvider); await app.register(errorHandlerPlugin); if (!process.env.RunEnvironment) { @@ -73,10 +75,10 @@ async function init() { if (import.meta.url === `file://${process.argv[1]}`) { // local development const app = await init(); - app.listen({ port: 3000 }, (err) => { + app.listen({ port: 8080 }, (err) => { /* eslint no-console: ["error", {"allow": ["log", "error"]}] */ if (err) console.error(err); - console.log("Server listening on 3000"); + console.log("Server listening on 8080"); }); } export default init; diff --git a/src/plugins/validate.ts b/src/plugins/validate.ts new file mode 100644 index 0000000..12724b4 --- /dev/null +++ b/src/plugins/validate.ts @@ -0,0 +1,35 @@ +import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; +import fp from "fastify-plugin"; +import { InternalServerError, ValidationError } from "../errors/index.js"; +import { z, ZodError } from "zod"; +import { fromError } from "zod-validation-error"; + +const zodValidationPlugin: FastifyPluginAsync = async (fastify, _options) => { + fastify.decorate( + "zodValidateBody", + async function ( + request: FastifyRequest, + _reply: FastifyReply, + zodSchema: z.ZodTypeAny, + ): Promise { + try { + await zodSchema.parseAsync(request.body); + } catch (e: unknown) { + if (e instanceof ZodError) { + throw new ValidationError({ + message: fromError(e).toString().replace("Validation error: ", ""), + }); + } else if (e instanceof Error) { + request.log.error(`Error validating request body: ${e.toString()}`); + throw new InternalServerError({ + message: "Could not validate request body.", + }); + } + throw e; + } + }, + ); +}; + +const fastifyZodValidationPlugin = fp(zodValidationPlugin); +export default fastifyZodValidationPlugin; diff --git a/src/routes/events.ts b/src/routes/events.ts index 3c78b70..f86f3ae 100644 --- a/src/routes/events.ts +++ b/src/routes/events.ts @@ -17,18 +17,26 @@ import { randomUUID } from "crypto"; const repeatOptions = ["weekly", "biweekly"] as const; -const requestBodySchema = z.object({ +const baseBodySchema = z.object({ title: z.string().min(1), description: z.string().min(1), start: z.string(), end: z.optional(z.string()), location: z.string(), locationLink: z.optional(z.string().url()), - repeats: z.optional(z.enum(repeatOptions)), host: z.enum(OrganizationList), featured: z.boolean().default(false), }); -const requestJsonSchema = zodToJsonSchema(requestBodySchema); + +const requestBodySchema = baseBodySchema + .extend({ + repeats: z.optional(z.enum(repeatOptions)), + repeatEnds: z.string().optional(), + }) + .refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), { + message: "repeats is required when repeatEnds is defined", + }); + type EventPostRequest = z.infer; const responseJsonSchema = zodToJsonSchema( @@ -51,9 +59,11 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { "/:id?", { schema: { - body: requestJsonSchema, response: { 200: responseJsonSchema }, }, + preValidation: async (request, reply) => { + await fastify.zodValidateBody(request, reply, requestBodySchema); + }, onRequest: async (request, reply) => { await fastify.authorize(request, reply, [AppRoles.MANAGER]); }, diff --git a/src/types.d.ts b/src/types.d.ts index 7f13dd3..7ca8dc6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -12,6 +12,11 @@ declare module "fastify" { reply: FastifyReply, validRoles: AppRoles[], ) => Promise; + zodValidateBody: ( + request: FastifyRequest, + _reply: FastifyReply, + zodSchema: Zod.ZodTypeAny, + ) => Promise; runEnvironment: RunEnvironment; } interface FastifyRequest { diff --git a/yarn.lock b/yarn.lock index b60eafe..e316f0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4724,6 +4724,11 @@ zod-to-json-schema@^3.23.2: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz#bc7e379c8050462538383e382964c03d8fe008f9" integrity sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw== +zod-validation-error@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.1.tgz#86adc781129d1a7fed3c3e567e8dbe7c4a15eaa4" + integrity sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA== + zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"