From 95e00816c409c695071a7cdbe6300759aa37ec89 Mon Sep 17 00:00:00 2001
From: Lars Hampe <hello@hashdot.co>
Date: Wed, 6 Nov 2024 22:04:20 +0100
Subject: [PATCH] feat(api): more refactor

---
 apps/api/src/changelog/version/byId.ts       |  79 ++++++-----
 apps/api/src/changelog/version/create.ts     | 135 +++++++++----------
 apps/api/src/changelog/version/createAuto.ts |  12 +-
 apps/api/src/changelog/version/delete.ts     |  93 ++++++-------
 apps/api/src/changelog/version/index.ts      | 125 ++---------------
 apps/api/src/changelog/version/update.ts     | 102 +++++++-------
 packages/schema/src/version/base.ts          |   2 +-
 packages/schema/src/version/create.ts        |   2 +-
 8 files changed, 222 insertions(+), 328 deletions(-)

diff --git a/apps/api/src/changelog/version/byId.ts b/apps/api/src/changelog/version/byId.ts
index 0eee95f..194a3dc 100644
--- a/apps/api/src/changelog/version/byId.ts
+++ b/apps/api/src/changelog/version/byId.ts
@@ -3,6 +3,11 @@ import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
 import { createRoute } from '@hono/zod-openapi'
 import { and, eq } from 'drizzle-orm'
 
+import { HTTPException } from 'hono/http-exception'
+import type changelogVersionApi from '.'
+import { verifyAuthentication } from '../../utils/authentication'
+import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
+
 export const byId = createRoute({
   method: 'get',
   path: '/:id',
@@ -19,49 +24,41 @@ export const byId = createRoute({
       },
       description: 'Return version by id',
     },
-    400: {
-      description: 'Bad Request',
-    },
-    500: {
-      description: 'Internal Server Error',
-    },
+    ...openApiErrorResponses,
   },
+  ...openApiSecurity,
 })
 
-export const byIdFunc = async ({
-  userId,
-  id,
-}: {
-  userId: string
-  id: string
-}) => {
-  const versionResult = await db.query.changelog_version.findFirst({
-    where: eq(changelog_version.id, id),
-    with: {
-      commits: true,
-    },
+export const registerVersionById = (api: typeof changelogVersionApi) => {
+  return api.openapi(byId, async (c) => {
+    const userId = await verifyAuthentication(c)
+    const { id } = c.req.valid('param')
+
+    const versionResult = await db.query.changelog_version.findFirst({
+      where: eq(changelog_version.id, id),
+    })
+
+    if (!versionResult) {
+      throw new HTTPException(404, { message: 'Not Found' })
+    }
+
+    if (!versionResult.changelogId) {
+      throw new HTTPException(404, { message: 'Not Found' })
+    }
+
+    const changelogResult = await db.query.changelog.findMany({
+      where: and(eq(changelog.userId, userId)),
+      columns: {
+        id: true,
+      },
+    })
+
+    const changelogIds = changelogResult.map((cl) => cl.id)
+
+    if (!changelogIds.includes(versionResult.changelogId)) {
+      throw new HTTPException(404, { message: 'Not Found' })
+    }
+
+    return c.json(VersionOutput.parse(versionResult), 200)
   })
-
-  if (!versionResult) {
-    return null
-  }
-
-  if (!versionResult.changelogId) {
-    return null
-  }
-
-  const changelogResult = await db.query.changelog.findMany({
-    where: and(eq(changelog.userId, userId)),
-    columns: {
-      id: true,
-    },
-  })
-
-  const changelogIds = changelogResult.map((cl) => cl.id)
-
-  if (!changelogIds.includes(versionResult.changelogId)) {
-    return null
-  }
-
-  return versionResult
 }
diff --git a/apps/api/src/changelog/version/create.ts b/apps/api/src/changelog/version/create.ts
index 36f15cd..74795ee 100644
--- a/apps/api/src/changelog/version/create.ts
+++ b/apps/api/src/changelog/version/create.ts
@@ -9,9 +9,13 @@ import { createRoute, type z } from '@hono/zod-openapi'
 import { and, eq, inArray } from 'drizzle-orm'
 import { HTTPException } from 'hono/http-exception'
 import semver from 'semver'
+
+import type changelogVersionApi from '.'
+import { verifyAuthentication } from '../../utils/authentication'
+import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
 import { redis } from '../../utils/redis'
 
-export const create = createRoute({
+export const route = createRoute({
   method: 'post',
   path: '/',
   tags: ['version'],
@@ -29,76 +33,71 @@ export const create = createRoute({
       },
       description: 'Version created',
     },
-    400: {
-      description: 'Bad Request',
-    },
-    500: {
-      description: 'Internal Server Error',
-    },
+    ...openApiErrorResponses,
   },
+  ...openApiSecurity,
 })
 
-export const createFunc = async ({
-  userId,
-  payload,
-}: {
-  userId: string
-  payload: z.infer<typeof VersionCreateInput>
-}) => {
-  const changelogResult = await db.query.changelog.findFirst({
-    where: and(
-      eq(changelog.userId, userId),
-      eq(changelog.id, payload.changelogId),
-    ),
-    with: {
-      versions: {
-        where: and(
-          eq(changelog_version.changelogId, payload.changelogId),
-          eq(changelog_version.version, payload.version),
-        ),
+export const registerVersionCreate = (api: typeof changelogVersionApi) => {
+  return api.openapi(route, async (c) => {
+    const userId = await verifyAuthentication(c)
+    const payload: z.infer<typeof VersionCreateInput> = await c.req.json()
+
+    const changelogResult = await db.query.changelog.findFirst({
+      where: and(
+        eq(changelog.userId, userId),
+        eq(changelog.id, payload.changelogId),
+      ),
+      with: {
+        versions: {
+          where: and(
+            eq(changelog_version.changelogId, payload.changelogId),
+            eq(changelog_version.version, payload.version),
+          ),
+        },
       },
-    },
+    })
+
+    if (!changelogResult) {
+      throw new HTTPException(404, {
+        message: 'Changelog not found',
+      })
+    }
+
+    if (changelogResult.versions.length) {
+      throw new HTTPException(409, {
+        message: 'Version exists already',
+      })
+    }
+
+    const formattedVersion = semver.coerce(payload.version)
+    const validVersion = semver.valid(formattedVersion)
+
+    if (validVersion === null) {
+      throw new HTTPException(409, {
+        message: 'Version is not semver compatible',
+      })
+    }
+
+    const [versionCreateResult] = await db
+      .insert(changelog_version)
+      .values({
+        changelogId: payload.changelogId,
+        version: validVersion,
+        status: payload.status,
+        markdown: payload.markdown,
+      })
+      .returning()
+
+    if (changelogResult.pageId) {
+      redis.del(changelogResult.pageId)
+    }
+
+    await db
+      .update(changelog_commit)
+      .set({ versionId: versionCreateResult.id })
+      .where(inArray(changelog_commit.id, payload.commitIds))
+
+    return c.json(VersionCreateOutput.parse(versionCreateResult), 201)
   })
-
-  if (!changelogResult) {
-    throw new HTTPException(404, {
-      message: 'Changelog not found',
-    })
-  }
-
-  if (changelogResult.versions.length) {
-    throw new HTTPException(409, {
-      message: 'Version exists already',
-    })
-  }
-
-  const formattedVersion = semver.coerce(payload.version)
-  const validVersion = semver.valid(formattedVersion)
-
-  if (validVersion === null) {
-    throw new HTTPException(409, {
-      message: 'Version is not semver compatible',
-    })
-  }
-
-  const [versionCreateResult] = await db
-    .insert(changelog_version)
-    .values({
-      changelogId: payload.changelogId,
-      version: validVersion,
-      status: payload.status,
-      markdown: payload.markdown,
-    })
-    .returning()
-
-  if (changelogResult.pageId) {
-    redis.del(changelogResult.pageId)
-  }
-
-  await db
-    .update(changelog_commit)
-    .set({ versionId: versionCreateResult.id })
-    .where(inArray(changelog_commit.id, payload.commitIds))
-
-  return versionCreateResult
 }
diff --git a/apps/api/src/changelog/version/createAuto.ts b/apps/api/src/changelog/version/createAuto.ts
index 592c74d..84964f4 100644
--- a/apps/api/src/changelog/version/createAuto.ts
+++ b/apps/api/src/changelog/version/createAuto.ts
@@ -13,9 +13,11 @@ import { format } from 'date-fns'
 import { and, eq, isNull } from 'drizzle-orm'
 import { HTTPException } from 'hono/http-exception'
 import semver from 'semver'
+
 import type { changelogVersionApi } from '.'
 import { verifyAuthentication } from '../../utils/authentication'
 import { commitsToMarkdown } from '../../utils/git/commitsToMarkdown'
+import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
 import { redis } from '../../utils/redis'
 
 export const route = createRoute({
@@ -36,13 +38,9 @@ export const route = createRoute({
       },
       description: 'Commits created',
     },
-    400: {
-      description: 'Bad Request',
-    },
-    500: {
-      description: 'Internal Server Error',
-    },
+    ...openApiErrorResponses,
   },
+  ...openApiSecurity,
 })
 
 const getVersion = (version: string) => {
@@ -173,6 +171,6 @@ export const registerVersionCreateAuto = (api: typeof changelogVersionApi) => {
       redis.del(changelogResult.pageId)
     }
 
-    return c.json(versionCreateResult, 201)
+    return c.json(VersionCreateOutput.parse(versionCreateResult), 201)
   })
 }
diff --git a/apps/api/src/changelog/version/delete.ts b/apps/api/src/changelog/version/delete.ts
index a7cbad8..e4e5176 100644
--- a/apps/api/src/changelog/version/delete.ts
+++ b/apps/api/src/changelog/version/delete.ts
@@ -8,9 +8,12 @@ import { GeneralOutput } from '@boring.tools/schema'
 import { createRoute } from '@hono/zod-openapi'
 import { and, eq } from 'drizzle-orm'
 import { HTTPException } from 'hono/http-exception'
+import type changelogVersionApi from '.'
+import { verifyAuthentication } from '../../utils/authentication'
+import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
 import { redis } from '../../utils/redis'
 
-export const remove = createRoute({
+export const route = createRoute({
   method: 'delete',
   path: '/:id',
   tags: ['version'],
@@ -23,57 +26,55 @@ export const remove = createRoute({
       },
       description: 'Removes a version by id',
     },
-    404: {
-      content: {
-        'application/json': {
-          schema: GeneralOutput,
-        },
-      },
-      description: 'Version not found',
-    },
-    500: {
-      description: 'Internal Server Error',
-    },
+    ...openApiErrorResponses,
   },
+  ...openApiSecurity,
 })
 
-export const removeFunc = async ({
-  userId,
-  id,
-}: {
-  userId: string
-  id: string
-}) => {
-  const changelogResult = await db.query.changelog.findMany({
-    where: and(eq(changelog.userId, userId)),
-    with: {
-      versions: {
-        where: eq(changelog_version.id, id),
+export const registerVersionDelete = async (
+  api: typeof changelogVersionApi,
+) => {
+  return api.openapi(route, async (c) => {
+    const userId = await verifyAuthentication(c)
+    const id = c.req.param('id')
+
+    const changelogResult = await db.query.changelog.findMany({
+      where: and(eq(changelog.userId, userId)),
+      with: {
+        versions: {
+          where: eq(changelog_version.id, id),
+        },
       },
-    },
-  })
-
-  const findChangelog = changelogResult.find((change) =>
-    change.versions.find((ver) => ver.id === id),
-  )
-
-  if (!findChangelog?.versions.length) {
-    throw new HTTPException(404, {
-      message: 'Version not found',
     })
-  }
 
-  await db
-    .update(changelog_commit)
-    .set({ versionId: null })
-    .where(eq(changelog_commit.versionId, id))
+    if (!changelogResult) {
+      throw new HTTPException(404, { message: 'Not Found' })
+    }
 
-  if (findChangelog.pageId) {
-    redis.del(findChangelog.pageId)
-  }
+    const findChangelog = changelogResult.find((change) =>
+      change.versions.find((ver) => ver.id === id),
+    )
 
-  return db
-    .delete(changelog_version)
-    .where(and(eq(changelog_version.id, id)))
-    .returning()
+    if (!findChangelog?.versions.length) {
+      throw new HTTPException(404, {
+        message: 'Version not found',
+      })
+    }
+
+    await db
+      .update(changelog_commit)
+      .set({ versionId: null })
+      .where(eq(changelog_commit.versionId, id))
+
+    if (findChangelog.pageId) {
+      redis.del(findChangelog.pageId)
+    }
+
+    await db
+      .delete(changelog_version)
+      .where(and(eq(changelog_version.id, id)))
+      .returning()
+
+    return c.json(GeneralOutput.parse({ message: 'Version deleted' }), 200)
+  })
 }
diff --git a/apps/api/src/changelog/version/index.ts b/apps/api/src/changelog/version/index.ts
index 7bb0f35..52d7706 100644
--- a/apps/api/src/changelog/version/index.ts
+++ b/apps/api/src/changelog/version/index.ts
@@ -1,12 +1,12 @@
 import { OpenAPIHono } from '@hono/zod-openapi'
+
 import type { Variables } from '../..'
-import { verifyAuthentication } from '../../utils/authentication'
-import { type ContextModule, captureSentry } from '../../utils/sentry'
-import { byId, byIdFunc } from './byId'
-import { create, createFunc } from './create'
+import type { ContextModule } from '../../utils/sentry'
+import { registerVersionById } from './byId'
+import { registerVersionCreate } from './create'
 import { registerVersionCreateAuto } from './createAuto'
-import { remove, removeFunc } from './delete'
-import { update, updateFunc } from './update'
+import { registerVersionDelete } from './delete'
+import { registerVersionUpdate } from './update'
 
 export const changelogVersionApi = new OpenAPIHono<{ Variables: Variables }>()
 
@@ -16,114 +16,9 @@ const module: ContextModule = {
 }
 
 registerVersionCreateAuto(changelogVersionApi)
-
-changelogVersionApi.openapi(create, async (c) => {
-  const userId = await verifyAuthentication(c)
-  try {
-    const payload = await c.req.json()
-    const result = await createFunc({ userId, payload })
-
-    if (!result) {
-      return c.json({ message: 'Version not created' }, 400)
-    }
-
-    return c.json(result, 201)
-  } catch (error) {
-    return captureSentry({
-      c,
-      error,
-      module,
-      user: {
-        id: userId,
-      },
-    })
-  }
-})
-
-changelogVersionApi.openapi(byId, async (c) => {
-  const userId = await verifyAuthentication(c)
-  try {
-    const id = c.req.param('id')
-    const result = await byIdFunc({ userId, id })
-
-    if (!result) {
-      return c.json({ message: 'Version not found' }, 404)
-    }
-
-    // Ensure all required properties are present and non-null
-    return c.json(
-      {
-        ...result,
-        changelogId: result.changelogId || '',
-        version: result.version || '',
-        status: result.status || 'draft',
-        releasedAt: result.releasedAt,
-        commits: result.commits || [],
-        markdown: result.markdown || '',
-      },
-      200,
-    )
-  } catch (error) {
-    return captureSentry({
-      c,
-      error,
-      module,
-      user: {
-        id: userId,
-      },
-    })
-  }
-})
-
-changelogVersionApi.openapi(update, async (c) => {
-  const userId = await verifyAuthentication(c)
-  try {
-    const id = c.req.param('id')
-
-    if (!id) {
-      return c.json({ message: 'Version not found' }, 404)
-    }
-
-    const result = await updateFunc({
-      userId,
-      payload: await c.req.json(),
-      id,
-    })
-
-    return c.json(result)
-  } catch (error) {
-    return captureSentry({
-      c,
-      error,
-      module,
-      user: {
-        id: userId,
-      },
-    })
-  }
-})
-
-changelogVersionApi.openapi(remove, async (c) => {
-  const userId = await verifyAuthentication(c)
-  try {
-    const id = c.req.param('id')
-    const result = await removeFunc({ userId, id })
-
-    if (result.length === 0) {
-      return c.json({ message: 'Version not found' }, 404)
-    }
-
-    return c.json({ message: 'Version removed' })
-  } catch (error) {
-    return captureSentry({
-      c,
-      error,
-      module,
-      user: {
-        id: userId,
-      },
-    })
-  }
-})
+registerVersionById(changelogVersionApi)
+registerVersionCreate(changelogVersionApi)
+registerVersionDelete(changelogVersionApi)
+registerVersionUpdate(changelogVersionApi)
 
 export default changelogVersionApi
diff --git a/apps/api/src/changelog/version/update.ts b/apps/api/src/changelog/version/update.ts
index 4d3501d..f288875 100644
--- a/apps/api/src/changelog/version/update.ts
+++ b/apps/api/src/changelog/version/update.ts
@@ -3,9 +3,13 @@ import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
 import { createRoute, type z } from '@hono/zod-openapi'
 import { and, eq } from 'drizzle-orm'
 import { HTTPException } from 'hono/http-exception'
+
+import type changelogVersionApi from '.'
+import { verifyAuthentication } from '../../utils/authentication'
+import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
 import { redis } from '../../utils/redis'
 
-export const update = createRoute({
+export const route = createRoute({
   method: 'put',
   path: '/:id',
   tags: ['version'],
@@ -23,57 +27,57 @@ export const update = createRoute({
       },
       description: 'Return updated version',
     },
-    400: {
-      description: 'Bad Request',
-    },
-    500: {
-      description: 'Internal Server Error',
-    },
+    ...openApiErrorResponses,
   },
+  ...openApiSecurity,
 })
 
-export const updateFunc = async ({
-  userId,
-  payload,
-  id,
-}: {
-  userId: string
-  payload: z.infer<typeof VersionUpdateInput>
-  id: string
-}) => {
-  const changelogResult = await db.query.changelog.findMany({
-    where: and(eq(changelog.userId, userId)),
-    with: {
-      versions: {
-        where: eq(changelog_version.id, id),
+export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
+  return api.openapi(route, async (c) => {
+    const userId = await verifyAuthentication(c)
+    const id = c.req.param('id')
+    const payload: z.infer<typeof VersionUpdateInput> = await c.req.json()
+
+    const changelogResult = await db.query.changelog.findMany({
+      where: and(eq(changelog.userId, userId)),
+      with: {
+        versions: {
+          where: eq(changelog_version.id, id),
+        },
       },
-    },
+    })
+
+    if (!changelogResult) {
+      throw new HTTPException(404, {
+        message: 'Version not found',
+      })
+    }
+
+    const findChangelog = changelogResult.find((change) =>
+      change.versions.find((ver) => ver.id === id),
+    )
+
+    if (!findChangelog?.versions.length) {
+      throw new HTTPException(404, {
+        message: 'Version not found',
+      })
+    }
+
+    const [versionUpdateResult] = await db
+      .update(changelog_version)
+      .set({
+        version: payload.version,
+        status: payload.status,
+        markdown: payload.markdown,
+        releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
+      })
+      .where(and(eq(changelog_version.id, id)))
+      .returning()
+
+    if (findChangelog.pageId) {
+      redis.del(findChangelog.pageId)
+    }
+
+    return c.json(VersionUpdateOutput.parse(versionUpdateResult), 200)
   })
-
-  const findChangelog = changelogResult.find((change) =>
-    change.versions.find((ver) => ver.id === id),
-  )
-
-  if (!findChangelog?.versions.length) {
-    throw new HTTPException(404, {
-      message: 'Version not found',
-    })
-  }
-
-  const [versionUpdateResult] = await db
-    .update(changelog_version)
-    .set({
-      version: payload.version,
-      status: payload.status,
-      markdown: payload.markdown,
-      releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null,
-    })
-    .where(and(eq(changelog_version.id, id)))
-    .returning()
-
-  if (findChangelog.pageId) {
-    redis.del(findChangelog.pageId)
-  }
-
-  return versionUpdateResult
 }
diff --git a/packages/schema/src/version/base.ts b/packages/schema/src/version/base.ts
index 8ca5828..08d5a4c 100644
--- a/packages/schema/src/version/base.ts
+++ b/packages/schema/src/version/base.ts
@@ -14,7 +14,7 @@ export const VersionOutput = z
     markdown: z.string().openapi({
       example: '### Changelog\n\n- Added a new feature',
     }),
-    releasedAt: z.date().optional().openapi({
+    releasedAt: z.date().optional().nullable().openapi({
       example: '2024-01-01T00:00:00.000Z',
     }),
     status: z.enum(['draft', 'review', 'published']).default('draft').openapi({
diff --git a/packages/schema/src/version/create.ts b/packages/schema/src/version/create.ts
index 0998631..2f6c19e 100644
--- a/packages/schema/src/version/create.ts
+++ b/packages/schema/src/version/create.ts
@@ -6,7 +6,7 @@ export const VersionCreateInput = z
   .object({
     changelogId: z.string().uuid(),
     version: z.string(),
-    releasedAt: z.date().or(z.string()).optional(),
+    releasedAt: z.date().or(z.string()).optional().nullable(),
     status: z.enum(['draft', 'review', 'published']).default('draft'),
     markdown: z.string(),
     commitIds: z.array(z.string()),