From 1729766d068e9f69618ed4b5dbb29bccb6f367b0 Mon Sep 17 00:00:00 2001 From: michivonah Date: Fri, 31 Oct 2025 19:35:04 +0100 Subject: [PATCH] refactor validation by using zod + caching for more endpoints --- api/src/lib/cache.ts | 10 ++++++++- api/src/lib/http-z-validator.ts | 19 +++++++++++++++-- api/src/routes/attraction.ts | 30 +++++++++++---------------- api/src/routes/logbook.ts | 3 +-- api/src/routes/notification-method.ts | 28 ++++++++++++++----------- api/src/routes/themepark.ts | 5 +++-- api/src/routes/user.ts | 2 +- 7 files changed, 59 insertions(+), 38 deletions(-) diff --git a/api/src/lib/cache.ts b/api/src/lib/cache.ts index 33124ce..b631337 100644 --- a/api/src/lib/cache.ts +++ b/api/src/lib/cache.ts @@ -1,9 +1,17 @@ import { cache } from 'hono/cache' /** - * Cache unit to use for multiple endpoints as needed + * Cache unit to use for multiple endpoints as needed (TTL: 86400s) */ export const responseCache = cache({ cacheName: 'themepark-assistant', cacheControl: 'max-age=86400' +}); + +/** + * Cache for dynamic data (TTL: 30s) + */ +export const dynamicCache = cache({ + cacheName: 'themepark-assistant-dynamic', + cacheControl: 'max-age=30s' }); \ No newline at end of file diff --git a/api/src/lib/http-z-validator.ts b/api/src/lib/http-z-validator.ts index 3308ffe..ee9a787 100644 --- a/api/src/lib/http-z-validator.ts +++ b/api/src/lib/http-z-validator.ts @@ -8,8 +8,23 @@ import { InvalidParameter } from '../errors' * @param schema Zod Validation scheme (docs: https://zod.dev/api) * @returns zValidator for running the validation */ -export default function httpZValidator(type: 'query' | 'json' | 'param' = 'query', schema: T){ +export function httpZValidator(type: 'query' | 'json' | 'param' = 'query', schema: T){ return zValidator(type, schema, (result, c) => { if(!result.success) throw new InvalidParameter(); }) -} \ No newline at end of file +} + +// Predefined validators +/** + * Validates if id is a number (using zod) + */ +export const idValidator = httpZValidator('param', z.strictObject({ + id: z.coerce.number() +})); + +/** + * Validates if notificationMethodId is number (using zod) + */ +export const notificationMethodIdValidator = httpZValidator('query', z.strictObject({ + notificationMethodId: z.coerce.number() +})); \ No newline at end of file diff --git a/api/src/routes/attraction.ts b/api/src/routes/attraction.ts index a9f2a02..8e95071 100644 --- a/api/src/routes/attraction.ts +++ b/api/src/routes/attraction.ts @@ -1,4 +1,4 @@ -import { Hono, Context } from 'hono' +import { Hono } from 'hono' import { getDbContext } from '../db/client' import { attractionNotification } from '../db/schema' import { and, eq } from 'drizzle-orm' @@ -6,19 +6,14 @@ import { DatabaseError, InvalidParameter, MissingParameter } from '../errors' import { getUser } from '../lib/user-auth' import { Message } from '../types/response' import { getNotificationMethodOwner } from '../lib/check-notification-method-owner' +import { httpZValidator, idValidator, notificationMethodIdValidator } from '../lib/http-z-validator' +import * as z from 'zod' const app = new Hono() -/** - * Checks if request has notificationMethodId parameter - * @param c Request context - * @returns if available notificationMethodId, else undefined - */ -function getNotificationMethodId(c: Context): number | undefined{ - const str = c.req.query('notificationMethodId'); - if(!str) return undefined; - return parseInt(str); -} + + + /** * Checks if drizzle db error has a message cause @@ -37,13 +32,12 @@ function hasMessageCause(e: unknown): e is Error & { cause: { message: string }} /** * Subscribe to waittime notifications from a specified attraction */ -app.post('/:id/subscribe', async (c) => { - const attractionId = parseInt(c.req.param('id')); +app.post('/:id/subscribe', idValidator, notificationMethodIdValidator, async (c) => { + const attractionId = c.req.valid('param').id; const db = getDbContext(c) const user = await getUser(c); - const notificationMethodId = getNotificationMethodId(c); - if(!notificationMethodId) throw new MissingParameter('notificationMethodId'); + const notificationMethodId = c.req.valid('query').notificationMethodId; const method = await getNotificationMethodOwner(db, notificationMethodId, user.id); @@ -73,12 +67,12 @@ app.post('/:id/subscribe', async (c) => { /** * Unsubscribe to waittime notifications from a specified attraction */ -app.post('/:id/unsubscribe', async (c) => { - const attractionId = parseInt(c.req.param('id')); +app.post('/:id/unsubscribe', idValidator, notificationMethodIdValidator, async (c) => { + const attractionId = c.req.valid('param').id; const db = getDbContext(c) const user = await getUser(c); - const notificationMethodId = getNotificationMethodId(c); + const notificationMethodId = c.req.valid('query').notificationMethodId; const methodOwner = notificationMethodId ? await getNotificationMethodOwner(db, notificationMethodId, user.id): false; const queryConditions = [ diff --git a/api/src/routes/logbook.ts b/api/src/routes/logbook.ts index d13ceba..6edf27c 100644 --- a/api/src/routes/logbook.ts +++ b/api/src/routes/logbook.ts @@ -5,9 +5,8 @@ import { logbook } from '../db/schema' import { and, eq } from 'drizzle-orm' import { getUser } from '../lib/user-auth' import { Message } from '../types/response' - +import { httpZValidator } from '../lib/http-z-validator' import * as z from 'zod' -import httpZValidator from '../lib/http-z-validator' const app = new Hono() diff --git a/api/src/routes/notification-method.ts b/api/src/routes/notification-method.ts index 7dde507..a735af3 100644 --- a/api/src/routes/notification-method.ts +++ b/api/src/routes/notification-method.ts @@ -5,6 +5,9 @@ import { eq, and } from 'drizzle-orm' import { getUser } from '../lib/user-auth' import { DatabaseError, InvalidParameter, MissingParameter } from '../errors' import { Message } from '../types/response' +import { responseCache, dynamicCache } from '../lib/cache' +import { httpZValidator, idValidator } from '../lib/http-z-validator' +import * as z from 'zod' const app = new Hono() @@ -51,7 +54,7 @@ app.get('/list', async (c) => { }) /** Lists all available notification providers */ -app.get('/list-providers', async (c) => { +app.get('/list-providers', responseCache, async (c) => { const db = getDbContext(c); try{ @@ -68,21 +71,24 @@ app.get('/list-providers', async (c) => { }) /** Creates a new notification method from url, name & provider */ -app.post('/add-method', async (c) => { +app.post('/add-method', httpZValidator('query', z.strictObject({ + url: z.string(), + name: z.string(), + provider: z.string() +})), +async (c) => { const db = getDbContext(c); const user = await getUser(c); - const { url, name, provider } = c.req.query(); + const params = c.req.valid('query'); - if(!url || !name || !provider) throw new MissingParameter(); - - const providerId = await getProviderId(db, provider); + const providerId = await getProviderId(db, params.provider); if(!providerId) throw new InvalidParameter('provider'); try{ const newMethod = await db.insert(notificationMethod).values({ - webhookUrl: url, - shownName: name, + webhookUrl: params.url, + shownName: params.name, userId: user.id, notificationProviderId: providerId }).returning().onConflictDoNothing().get(); @@ -103,12 +109,10 @@ app.post('/add-method', async (c) => { }) /** Removes a existing notification method by id (has to be owned by the current user) */ -app.delete('/remove-method/:id', async (c) => { +app.delete('/remove-method/:id', idValidator, async (c) => { const db = getDbContext(c); const user = await getUser(c); - const methodId = parseInt(c.req.param('id')); - - if(!methodId) throw new InvalidParameter('id'); + const methodId = c.req.valid('param').id; try{ const res = await db.delete(notificationMethod).where( diff --git a/api/src/routes/themepark.ts b/api/src/routes/themepark.ts index 7bd89b9..ae2e7df 100644 --- a/api/src/routes/themepark.ts +++ b/api/src/routes/themepark.ts @@ -4,6 +4,7 @@ import { themepark, attraction } from '../db/schema' import { responseCache } from '../lib/cache' import { eq } from 'drizzle-orm' import { DatabaseError } from '../errors' +import { idValidator } from '../lib/http-z-validator' const app = new Hono() @@ -30,8 +31,8 @@ app.get('/list', responseCache, async (c) => { /** * Lists all attractions from a themepark with their id & name */ -app.get('/list/:id/attraction', responseCache, async (c) => { - const parkId = parseInt(c.req.param('id')); +app.get('/list/:id/attraction', responseCache, idValidator, async (c) => { + const parkId = c.req.valid('param').id; const db = getDbContext(c) try{ diff --git a/api/src/routes/user.ts b/api/src/routes/user.ts index 19e0f15..a6975a5 100644 --- a/api/src/routes/user.ts +++ b/api/src/routes/user.ts @@ -5,8 +5,8 @@ import { getUser } from '../lib/user-auth' import { user } from '../db/schema' import { eq } from 'drizzle-orm' import { Message } from '../types/response' +import { httpZValidator } from '../lib/http-z-validator' import * as z from 'zod' -import httpZValidator from '../lib/http-z-validator' const app = new Hono()