feat(api): more refactor

This commit is contained in:
Lars Hampe 2024-11-06 22:04:20 +01:00
parent bfc8ae2f21
commit 95e00816c4
8 changed files with 222 additions and 328 deletions

View File

@ -3,6 +3,11 @@ import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi' import { createRoute } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm' 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({ export const byId = createRoute({
method: 'get', method: 'get',
path: '/:id', path: '/:id',
@ -19,49 +24,41 @@ export const byId = createRoute({
}, },
description: 'Return version by id', description: 'Return version by id',
}, },
400: { ...openApiErrorResponses,
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
}, },
...openApiSecurity,
}) })
export const byIdFunc = async ({ export const registerVersionById = (api: typeof changelogVersionApi) => {
userId, return api.openapi(byId, async (c) => {
id, const userId = await verifyAuthentication(c)
}: { const { id } = c.req.valid('param')
userId: string
id: string const versionResult = await db.query.changelog_version.findFirst({
}) => { where: eq(changelog_version.id, id),
const versionResult = await db.query.changelog_version.findFirst({ })
where: eq(changelog_version.id, id),
with: { if (!versionResult) {
commits: true, 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
} }

View File

@ -9,9 +9,13 @@ import { createRoute, type z } from '@hono/zod-openapi'
import { and, eq, inArray } from 'drizzle-orm' import { and, eq, inArray } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import semver from 'semver' import semver from 'semver'
import type changelogVersionApi from '.'
import { verifyAuthentication } from '../../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
import { redis } from '../../utils/redis' import { redis } from '../../utils/redis'
export const create = createRoute({ export const route = createRoute({
method: 'post', method: 'post',
path: '/', path: '/',
tags: ['version'], tags: ['version'],
@ -29,76 +33,71 @@ export const create = createRoute({
}, },
description: 'Version created', description: 'Version created',
}, },
400: { ...openApiErrorResponses,
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
}, },
...openApiSecurity,
}) })
export const createFunc = async ({ export const registerVersionCreate = (api: typeof changelogVersionApi) => {
userId, return api.openapi(route, async (c) => {
payload, const userId = await verifyAuthentication(c)
}: { const payload: z.infer<typeof VersionCreateInput> = await c.req.json()
userId: string
payload: z.infer<typeof VersionCreateInput> const changelogResult = await db.query.changelog.findFirst({
}) => { where: and(
const changelogResult = await db.query.changelog.findFirst({ eq(changelog.userId, userId),
where: and( eq(changelog.id, payload.changelogId),
eq(changelog.userId, userId), ),
eq(changelog.id, payload.changelogId), with: {
), versions: {
with: { where: and(
versions: { eq(changelog_version.changelogId, payload.changelogId),
where: and( eq(changelog_version.version, payload.version),
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
} }

View File

@ -13,9 +13,11 @@ import { format } from 'date-fns'
import { and, eq, isNull } from 'drizzle-orm' import { and, eq, isNull } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception' import { HTTPException } from 'hono/http-exception'
import semver from 'semver' import semver from 'semver'
import type { changelogVersionApi } from '.' import type { changelogVersionApi } from '.'
import { verifyAuthentication } from '../../utils/authentication' import { verifyAuthentication } from '../../utils/authentication'
import { commitsToMarkdown } from '../../utils/git/commitsToMarkdown' import { commitsToMarkdown } from '../../utils/git/commitsToMarkdown'
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
import { redis } from '../../utils/redis' import { redis } from '../../utils/redis'
export const route = createRoute({ export const route = createRoute({
@ -36,13 +38,9 @@ export const route = createRoute({
}, },
description: 'Commits created', description: 'Commits created',
}, },
400: { ...openApiErrorResponses,
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
}, },
...openApiSecurity,
}) })
const getVersion = (version: string) => { const getVersion = (version: string) => {
@ -173,6 +171,6 @@ export const registerVersionCreateAuto = (api: typeof changelogVersionApi) => {
redis.del(changelogResult.pageId) redis.del(changelogResult.pageId)
} }
return c.json(versionCreateResult, 201) return c.json(VersionCreateOutput.parse(versionCreateResult), 201)
}) })
} }

View File

@ -8,9 +8,12 @@ import { GeneralOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi' import { createRoute } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception' 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' import { redis } from '../../utils/redis'
export const remove = createRoute({ export const route = createRoute({
method: 'delete', method: 'delete',
path: '/:id', path: '/:id',
tags: ['version'], tags: ['version'],
@ -23,57 +26,55 @@ export const remove = createRoute({
}, },
description: 'Removes a version by id', description: 'Removes a version by id',
}, },
404: { ...openApiErrorResponses,
content: {
'application/json': {
schema: GeneralOutput,
},
},
description: 'Version not found',
},
500: {
description: 'Internal Server Error',
},
}, },
...openApiSecurity,
}) })
export const removeFunc = async ({ export const registerVersionDelete = async (
userId, api: typeof changelogVersionApi,
id, ) => {
}: { return api.openapi(route, async (c) => {
userId: string const userId = await verifyAuthentication(c)
id: string const id = c.req.param('id')
}) => {
const changelogResult = await db.query.changelog.findMany({ const changelogResult = await db.query.changelog.findMany({
where: and(eq(changelog.userId, userId)), where: and(eq(changelog.userId, userId)),
with: { with: {
versions: { versions: {
where: eq(changelog_version.id, id), 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 if (!changelogResult) {
.update(changelog_commit) throw new HTTPException(404, { message: 'Not Found' })
.set({ versionId: null }) }
.where(eq(changelog_commit.versionId, id))
if (findChangelog.pageId) { const findChangelog = changelogResult.find((change) =>
redis.del(findChangelog.pageId) change.versions.find((ver) => ver.id === id),
} )
return db if (!findChangelog?.versions.length) {
.delete(changelog_version) throw new HTTPException(404, {
.where(and(eq(changelog_version.id, id))) message: 'Version not found',
.returning() })
}
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)
})
} }

View File

@ -1,12 +1,12 @@
import { OpenAPIHono } from '@hono/zod-openapi' import { OpenAPIHono } from '@hono/zod-openapi'
import type { Variables } from '../..' import type { Variables } from '../..'
import { verifyAuthentication } from '../../utils/authentication' import type { ContextModule } from '../../utils/sentry'
import { type ContextModule, captureSentry } from '../../utils/sentry' import { registerVersionById } from './byId'
import { byId, byIdFunc } from './byId' import { registerVersionCreate } from './create'
import { create, createFunc } from './create'
import { registerVersionCreateAuto } from './createAuto' import { registerVersionCreateAuto } from './createAuto'
import { remove, removeFunc } from './delete' import { registerVersionDelete } from './delete'
import { update, updateFunc } from './update' import { registerVersionUpdate } from './update'
export const changelogVersionApi = new OpenAPIHono<{ Variables: Variables }>() export const changelogVersionApi = new OpenAPIHono<{ Variables: Variables }>()
@ -16,114 +16,9 @@ const module: ContextModule = {
} }
registerVersionCreateAuto(changelogVersionApi) registerVersionCreateAuto(changelogVersionApi)
registerVersionById(changelogVersionApi)
changelogVersionApi.openapi(create, async (c) => { registerVersionCreate(changelogVersionApi)
const userId = await verifyAuthentication(c) registerVersionDelete(changelogVersionApi)
try { registerVersionUpdate(changelogVersionApi)
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,
},
})
}
})
export default changelogVersionApi export default changelogVersionApi

View File

@ -3,9 +3,13 @@ import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
import { createRoute, type z } from '@hono/zod-openapi' import { createRoute, type z } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception' 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' import { redis } from '../../utils/redis'
export const update = createRoute({ export const route = createRoute({
method: 'put', method: 'put',
path: '/:id', path: '/:id',
tags: ['version'], tags: ['version'],
@ -23,57 +27,57 @@ export const update = createRoute({
}, },
description: 'Return updated version', description: 'Return updated version',
}, },
400: { ...openApiErrorResponses,
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
}, },
...openApiSecurity,
}) })
export const updateFunc = async ({ export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
userId, return api.openapi(route, async (c) => {
payload, const userId = await verifyAuthentication(c)
id, const id = c.req.param('id')
}: { const payload: z.infer<typeof VersionUpdateInput> = await c.req.json()
userId: string
payload: z.infer<typeof VersionUpdateInput> const changelogResult = await db.query.changelog.findMany({
id: string where: and(eq(changelog.userId, userId)),
}) => { with: {
const changelogResult = await db.query.changelog.findMany({ versions: {
where: and(eq(changelog.userId, userId)), where: eq(changelog_version.id, id),
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
} }

View File

@ -14,7 +14,7 @@ export const VersionOutput = z
markdown: z.string().openapi({ markdown: z.string().openapi({
example: '### Changelog\n\n- Added a new feature', 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', example: '2024-01-01T00:00:00.000Z',
}), }),
status: z.enum(['draft', 'review', 'published']).default('draft').openapi({ status: z.enum(['draft', 'review', 'published']).default('draft').openapi({

View File

@ -6,7 +6,7 @@ export const VersionCreateInput = z
.object({ .object({
changelogId: z.string().uuid(), changelogId: z.string().uuid(),
version: z.string(), 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'), status: z.enum(['draft', 'review', 'published']).default('draft'),
markdown: z.string(), markdown: z.string(),
commitIds: z.array(z.string()), commitIds: z.array(z.string()),