From 739bbe935b62604ada51a85c45b5ddfc081bd418 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 15 Sep 2024 14:59:21 +0200 Subject: [PATCH] refactor(api): :recycle: More OpenAPI refactoring --- api/api/v1/notifications/:id/dismiss.ts | 68 ++-- api/api/v1/notifications/:id/index.ts | 128 +++++-- api/api/v1/notifications/clear/index.ts | 57 ++-- .../notifications/destroy_multiple/index.ts | 80 +++-- api/api/v1/notifications/index.ts | 317 +++++++++--------- api/api/v1/profile/avatar.ts | 59 +++- api/api/v1/profile/header.ts | 59 +++- 7 files changed, 471 insertions(+), 297 deletions(-) diff --git a/api/api/v1/notifications/:id/dismiss.ts b/api/api/v1/notifications/:id/dismiss.ts index ad2eb5fd..84ab1d26 100644 --- a/api/api/v1/notifications/:id/dismiss.ts +++ b/api/api/v1/notifications/:id/dismiss.ts @@ -1,9 +1,10 @@ -import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; -import { zValidator } from "@hono/zod-validator"; +import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { db } from "~/drizzle/db"; import { Notifications, RolePermissions } from "~/drizzle/schema"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -27,28 +28,45 @@ export const schemas = { }), }; -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - zValidator("param", schemas.param, handleZodError), - auth(meta.auth, meta.permissions), - async (context) => { - const { id } = context.req.valid("param"); - - const { user } = context.get("auth"); - if (!user) { - return context.json({ error: "Unauthorized" }, 401); - } - - await db - .update(Notifications) - .set({ - dismissed: true, - }) - .where(eq(Notifications.id, id)); - - return context.json({}); +const route = createRoute({ + method: "post", + path: "/api/v1/notifications/{id}/dismiss", + summary: "Dismiss notification", + middleware: [auth(meta.auth, meta.permissions)], + request: { + params: schemas.param, + }, + responses: { + 200: { + description: "Notification dismissed", }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { id } = context.req.valid("param"); + + const { user } = context.get("auth"); + if (!user) { + return context.json({ error: "Unauthorized" }, 401); + } + + await db + .update(Notifications) + .set({ + dismissed: true, + }) + .where(eq(Notifications.id, id)); + + return context.newResponse(null, 200); + }), ); diff --git a/api/api/v1/notifications/:id/index.ts b/api/api/v1/notifications/:id/index.ts index 8a00d120..2f616585 100644 --- a/api/api/v1/notifications/:id/index.ts +++ b/api/api/v1/notifications/:id/index.ts @@ -1,8 +1,14 @@ -import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; -import { zValidator } from "@hono/zod-validator"; +import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { z } from "zod"; -import { findManyNotifications } from "~/classes/functions/notification"; +import { + findManyNotifications, + notificationToApi, +} from "~/classes/functions/notification"; import { RolePermissions } from "~/drizzle/schema"; +import { Note } from "~/packages/database-interface/note"; +import { User } from "~/packages/database-interface/user"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -26,36 +32,90 @@ export const schemas = { }), }; -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - zValidator("param", schemas.param, handleZodError), - auth(meta.auth, meta.permissions), - async (context) => { - const { id } = context.req.valid("param"); - - const { user } = context.get("auth"); - if (!user) { - return context.json({ error: "Unauthorized" }, 401); - } - - const notification = ( - await findManyNotifications( - { - where: (notification, { eq }) => - eq(notification.id, id), - limit: 1, - }, - user.id, - ) - )[0]; - - if (!notification) { - return context.json({ error: "Notification not found" }, 404); - } - - return context.json(notification); +const route = createRoute({ + method: "get", + path: "/api/v1/notifications/{id}", + summary: "Get notification", + middleware: [auth(meta.auth, meta.permissions)], + request: { + params: schemas.param, + }, + responses: { + 200: { + description: "Notification", + schema: z.object({ + account: z.lazy(() => User.schema).nullable(), + created_at: z.string(), + id: z.string().uuid(), + status: z.lazy(() => Note.schema).optional(), + // TODO: Add reactions + type: z.enum([ + "mention", + "status", + "follow", + "follow_request", + "reblog", + "poll", + "favourite", + "update", + "admin.sign_up", + "admin.report", + "chat", + "pleroma:chat_mention", + "pleroma:emoji_reaction", + "pleroma:event_reminder", + "pleroma:participation_request", + "pleroma:participation_accepted", + "move", + "group_reblog", + "group_favourite", + "user_approved", + ]), + target: z.lazy(() => User.schema).optional(), + }), }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + 404: { + description: "Notification not found", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { id } = context.req.valid("param"); + + const { user } = context.get("auth"); + if (!user) { + return context.json({ error: "Unauthorized" }, 401); + } + + const notification = ( + await findManyNotifications( + { + where: (notification, { eq }) => eq(notification.id, id), + limit: 1, + }, + user.id, + ) + )[0]; + + if (!notification) { + return context.json({ error: "Notification not found" }, 404); + } + + return context.json(await notificationToApi(notification), 200); + }), ); diff --git a/api/api/v1/notifications/clear/index.ts b/api/api/v1/notifications/clear/index.ts index f61ca99c..35b05190 100644 --- a/api/api/v1/notifications/clear/index.ts +++ b/api/api/v1/notifications/clear/index.ts @@ -1,7 +1,9 @@ import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { eq } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { Notifications, RolePermissions } from "~/drizzle/schema"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["POST"], @@ -19,25 +21,40 @@ export const meta = applyConfig({ }, }); -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - auth(meta.auth, meta.permissions), - async (context) => { - const { user } = context.get("auth"); - if (!user) { - return context.json({ error: "Unauthorized" }, 401); - } - - await db - .update(Notifications) - .set({ - dismissed: true, - }) - .where(eq(Notifications.notifiedId, user.id)); - - return context.json({}); +const route = createRoute({ + method: "post", + path: "/api/v1/notifications/clear", + summary: "Clear notifications", + middleware: [auth(meta.auth, meta.permissions)], + responses: { + 200: { + description: "Notifications cleared", }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user } = context.get("auth"); + if (!user) { + return context.json({ error: "Unauthorized" }, 401); + } + + await db + .update(Notifications) + .set({ + dismissed: true, + }) + .where(eq(Notifications.notifiedId, user.id)); + + return context.newResponse(null, 200); + }), ); diff --git a/api/api/v1/notifications/destroy_multiple/index.ts b/api/api/v1/notifications/destroy_multiple/index.ts index 8f6cc0e1..684e6eeb 100644 --- a/api/api/v1/notifications/destroy_multiple/index.ts +++ b/api/api/v1/notifications/destroy_multiple/index.ts @@ -1,9 +1,10 @@ -import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; -import { zValidator } from "@hono/zod-validator"; +import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { and, eq, inArray } from "drizzle-orm"; import { z } from "zod"; import { db } from "~/drizzle/db"; import { Notifications, RolePermissions } from "~/drizzle/schema"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["DELETE"], @@ -27,34 +28,51 @@ export const schemas = { }), }; -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - zValidator("query", schemas.query, handleZodError), - auth(meta.auth, meta.permissions), - async (context) => { - const { user } = context.get("auth"); - - if (!user) { - return context.json({ error: "Unauthorized" }, 401); - } - - const { "ids[]": ids } = context.req.valid("query"); - - await db - .update(Notifications) - .set({ - dismissed: true, - }) - .where( - and( - inArray(Notifications.id, ids), - eq(Notifications.notifiedId, user.id), - ), - ); - - return context.json({}); +const route = createRoute({ + method: "delete", + path: "/api/v1/notifications/destroy_multiple", + summary: "Dismiss multiple notifications", + middleware: [auth(meta.auth, meta.permissions)], + request: { + query: schemas.query, + }, + responses: { + 200: { + description: "Notifications dismissed", }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user } = context.get("auth"); + + if (!user) { + return context.json({ error: "Unauthorized" }, 401); + } + + const { "ids[]": ids } = context.req.valid("query"); + + await db + .update(Notifications) + .set({ + dismissed: true, + }) + .where( + and( + inArray(Notifications.id, ids), + eq(Notifications.notifiedId, user.id), + ), + ); + + return context.newResponse(null, 200); + }), ); diff --git a/api/api/v1/notifications/index.ts b/api/api/v1/notifications/index.ts index b9bf92ff..3b7382df 100644 --- a/api/api/v1/notifications/index.ts +++ b/api/api/v1/notifications/index.ts @@ -1,12 +1,6 @@ -import { - apiRoute, - applyConfig, - auth, - handleZodError, - idValidator, -} from "@/api"; +import { apiRoute, applyConfig, auth, idValidator } from "@/api"; import { fetchTimeline } from "@/timelines"; -import { zValidator } from "@hono/zod-validator"; +import { createRoute } from "@hono/zod-openapi"; import { sql } from "drizzle-orm"; import { z } from "zod"; import { @@ -15,6 +9,9 @@ import { } from "~/classes/functions/notification"; import type { NotificationWithRelations } from "~/classes/functions/notification"; import { RolePermissions } from "~/drizzle/schema"; +import { Note } from "~/packages/database-interface/note"; +import { User } from "~/packages/database-interface/user"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -36,137 +33,157 @@ export const meta = applyConfig({ }); export const schemas = { - query: z.object({ - max_id: z.string().regex(idValidator).optional(), - since_id: z.string().regex(idValidator).optional(), - min_id: z.string().regex(idValidator).optional(), - limit: z.coerce.number().int().min(1).max(80).default(15), - exclude_types: z - .enum([ - "mention", - "status", - "follow", - "follow_request", - "reblog", - "poll", - "favourite", - "update", - "admin.sign_up", - "admin.report", - "chat", - "pleroma:chat_mention", - "pleroma:emoji_reaction", - "pleroma:event_reminder", - "pleroma:participation_request", - "pleroma:participation_accepted", - "move", - "group_reblog", - "group_favourite", - "user_approved", - ]) - .array() - .optional(), - types: z - .enum([ - "mention", - "status", - "follow", - "follow_request", - "reblog", - "poll", - "favourite", - "update", - "admin.sign_up", - "admin.report", - "chat", - "pleroma:chat_mention", - "pleroma:emoji_reaction", - "pleroma:event_reminder", - "pleroma:participation_request", - "pleroma:participation_accepted", - "move", - "group_reblog", - "group_favourite", - "user_approved", - ]) - .array() - .optional(), - account_id: z.string().regex(idValidator).optional(), - }), + query: z + .object({ + max_id: z.string().regex(idValidator).optional(), + since_id: z.string().regex(idValidator).optional(), + min_id: z.string().regex(idValidator).optional(), + limit: z.coerce.number().int().min(1).max(80).default(15), + exclude_types: z + .enum([ + "mention", + "status", + "follow", + "follow_request", + "reblog", + "poll", + "favourite", + "update", + "admin.sign_up", + "admin.report", + "chat", + "pleroma:chat_mention", + "pleroma:emoji_reaction", + "pleroma:event_reminder", + "pleroma:participation_request", + "pleroma:participation_accepted", + "move", + "group_reblog", + "group_favourite", + "user_approved", + ]) + .array() + .optional(), + types: z + .enum([ + "mention", + "status", + "follow", + "follow_request", + "reblog", + "poll", + "favourite", + "update", + "admin.sign_up", + "admin.report", + "chat", + "pleroma:chat_mention", + "pleroma:emoji_reaction", + "pleroma:event_reminder", + "pleroma:participation_request", + "pleroma:participation_accepted", + "move", + "group_reblog", + "group_favourite", + "user_approved", + ]) + .array() + .optional(), + account_id: z.string().regex(idValidator).optional(), + }) + .refine((val) => { + // Can't use both exclude_types and types + return !(val.exclude_types && val.types); + }), }; +const route = createRoute({ + method: "get", + path: "/api/v1/notifications", + summary: "Get notifications", + middleware: [auth(meta.auth, meta.permissions)], + request: { + query: schemas.query, + }, + responses: { + 200: { + description: "Notifications", + content: { + "application/json": { + schema: z.array( + z.object({ + account: z.lazy(() => User.schema).nullable(), + created_at: z.string(), + id: z.string().uuid(), + status: z.lazy(() => Note.schema).optional(), + // TODO: Add reactions + type: z.string(), + target: z.lazy(() => User.schema).optional(), + }), + ), + }, + }, + }, + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - zValidator("query", schemas.query, handleZodError), - auth(meta.auth, meta.permissions), - async (context) => { - const { user } = context.get("auth"); - if (!user) { - return context.json({ error: "Unauthorized" }, 401); - } + app.openapi(route, async (context) => { + const { user } = context.get("auth"); + if (!user) { + return context.json({ error: "Unauthorized" }, 401); + } - const { - account_id, - exclude_types, - limit, - max_id, - min_id, - since_id, - types, - } = context.req.valid("query"); + const { + account_id, + exclude_types, + limit, + max_id, + min_id, + since_id, + types, + } = context.req.valid("query"); - if (types && exclude_types) { - return context.json( - { - error: "Can't use both types and exclude_types", - }, - 400, - ); - } - - const { objects, link } = - await fetchTimeline( - findManyNotifications, - { - where: ( - // @ts-expect-error Yes I KNOW the types are wrong - notification, - // @ts-expect-error Yes I KNOW the types are wrong - { lt, gte, gt, and, eq, not, inArray }, - ) => - and( - max_id - ? lt(notification.id, max_id) - : undefined, - since_id - ? gte(notification.id, since_id) - : undefined, - min_id - ? gt(notification.id, min_id) - : undefined, - eq(notification.notifiedId, user.id), - eq(notification.dismissed, false), - account_id - ? eq(notification.accountId, account_id) - : undefined, - not(eq(notification.accountId, user.id)), - types - ? inArray(notification.type, types) - : undefined, - exclude_types - ? not( - inArray( - notification.type, - exclude_types, - ), - ) - : undefined, - // Don't show notes that have filtered words in them (via Notification.note.content via Notification.noteId) - // Filters in `Filters` table have keyword in `FilterKeywords` table (use LIKE) - // Filters table has a userId and a context which is an array - sql`NOT EXISTS ( + const { objects, link } = + await fetchTimeline( + findManyNotifications, + { + where: ( + // @ts-expect-error Yes I KNOW the types are wrong + notification, + // @ts-expect-error Yes I KNOW the types are wrong + { lt, gte, gt, and, eq, not, inArray }, + ) => + and( + max_id ? lt(notification.id, max_id) : undefined, + since_id + ? gte(notification.id, since_id) + : undefined, + min_id ? gt(notification.id, min_id) : undefined, + eq(notification.notifiedId, user.id), + eq(notification.dismissed, false), + account_id + ? eq(notification.accountId, account_id) + : undefined, + not(eq(notification.accountId, user.id)), + types + ? inArray(notification.type, types) + : undefined, + exclude_types + ? not(inArray(notification.type, exclude_types)) + : undefined, + // Don't show notes that have filtered words in them (via Notification.note.content via Notification.noteId) + // Filters in `Filters` table have keyword in `FilterKeywords` table (use LIKE) + // Filters table has a userId and a context which is an array + sql`NOT EXISTS ( SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} @@ -182,23 +199,21 @@ export default apiRoute((app) => ) AND "Filters"."context" @> ARRAY['notifications'] )`, - ), - limit, - // @ts-expect-error Yes I KNOW the types are wrong - orderBy: (notification, { desc }) => - desc(notification.id), - }, - context.req.raw, - user.id, - ); - - return context.json( - await Promise.all(objects.map((n) => notificationToApi(n))), - 200, - { - Link: link, + ), + limit, + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (notification, { desc }) => desc(notification.id), }, + context.req.raw, + user.id, ); - }, - ), + + return context.json( + await Promise.all(objects.map((n) => notificationToApi(n))), + 200, + { + Link: link, + }, + ); + }), ); diff --git a/api/api/v1/profile/avatar.ts b/api/api/v1/profile/avatar.ts index 76e9b918..278e8fca 100644 --- a/api/api/v1/profile/avatar.ts +++ b/api/api/v1/profile/avatar.ts @@ -1,5 +1,8 @@ import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { RolePermissions } from "~/drizzle/schema"; +import { User } from "~/packages/database-interface/user"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["DELETE"], @@ -16,23 +19,43 @@ export const meta = applyConfig({ }, }); -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - auth(meta.auth, meta.permissions), - async (context) => { - const { user: self } = context.get("auth"); - - if (!self) { - return context.json({ error: "Unauthorized" }, 401); - } - - await self.update({ - avatar: "", - }); - - return context.json(self.toApi(true)); +const route = createRoute({ + method: "delete", + path: "/api/v1/profile/avatar", + summary: "Delete avatar", + middleware: [auth(meta.auth, meta.permissions)], + responses: { + 200: { + description: "User", + content: { + "application/json": { + schema: User.schema, + }, + }, }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user: self } = context.get("auth"); + + if (!self) { + return context.json({ error: "Unauthorized" }, 401); + } + + await self.update({ + avatar: "", + }); + + return context.json(self.toApi(true), 200); + }), ); diff --git a/api/api/v1/profile/header.ts b/api/api/v1/profile/header.ts index 130d4f42..c05c6d8d 100644 --- a/api/api/v1/profile/header.ts +++ b/api/api/v1/profile/header.ts @@ -1,5 +1,8 @@ import { apiRoute, applyConfig, auth } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; import { RolePermissions } from "~/drizzle/schema"; +import { User } from "~/packages/database-interface/user"; +import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ allowedMethods: ["DELETE"], @@ -16,23 +19,43 @@ export const meta = applyConfig({ }, }); -export default apiRoute((app) => - app.on( - meta.allowedMethods, - meta.route, - auth(meta.auth, meta.permissions), - async (context) => { - const { user: self } = context.get("auth"); - - if (!self) { - return context.json({ error: "Unauthorized" }, 401); - } - - await self.update({ - header: "", - }); - - return context.json(self.toApi(true)); +const route = createRoute({ + method: "delete", + path: "/api/v1/profile/header", + summary: "Delete header", + middleware: [auth(meta.auth, meta.permissions)], + responses: { + 200: { + description: "User", + content: { + "application/json": { + schema: User.schema, + }, + }, }, - ), + 401: { + description: "Unauthorized", + content: { + "application/json": { + schema: ErrorSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user: self } = context.get("auth"); + + if (!self) { + return context.json({ error: "Unauthorized" }, 401); + } + + await self.update({ + header: "", + }); + + return context.json(self.toApi(true), 200); + }), );