feat: refactor zod validations and openapi errors

This commit is contained in:
Lars Hampe 2024-11-06 21:33:57 +01:00
parent 34bc012ceb
commit bfc8ae2f21
33 changed files with 245 additions and 140 deletions

View File

@ -3,8 +3,10 @@ import { access_token, db } from '@boring.tools/database'
import { AccessTokenCreateInput, AccessTokenOutput } from '@boring.tools/schema'
import { createRoute, type z } from '@hono/zod-openapi'
import { HTTPException } from 'hono/http-exception'
import type { accessTokenApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
export const route = createRoute({
method: 'post',
@ -24,13 +26,9 @@ export const route = createRoute({
},
description: 'Commits created',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerAccessTokenCreate = (api: typeof accessTokenApi) => {
@ -53,6 +51,6 @@ export const registerAccessTokenCreate = (api: typeof accessTokenApi) => {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json(AccessTokenOutput.parse(result), 201)
})
}

View File

@ -3,27 +3,25 @@ import { GeneralOutput } 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 { accessTokenApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
export const route = createRoute({
method: 'delete',
path: '/:id',
tags: ['access-token'],
responses: {
201: {
200: {
content: {
'application/json': { schema: GeneralOutput },
},
description: 'Removes a access token by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
@ -40,6 +38,6 @@ export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json(GeneralOutput.parse({ message: 'Access token deleted' }), 200)
})
}

View File

@ -3,8 +3,10 @@ import { AccessTokenListOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import type { accessTokenApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
const route = createRoute({
method: 'get',
@ -19,13 +21,9 @@ const route = createRoute({
},
description: 'Return version by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerAccessTokenList = (api: typeof accessTokenApi) => {
@ -44,6 +42,6 @@ export const registerAccessTokenList = (api: typeof accessTokenApi) => {
token: at.token.substring(0, 10),
}))
return c.json(mappedData, 200)
return c.json(AccessTokenListOutput.parse(mappedData), 200)
})
}

View File

@ -3,8 +3,10 @@ import { CommitCreateInput, CommitCreateOutput } 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 { changelogCommitApi } from '.'
import { verifyAuthentication } from '../../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
export const route = createRoute({
method: 'post',
@ -24,13 +26,9 @@ export const route = createRoute({
},
description: 'Commits created',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerCommitCreate = (api: typeof changelogCommitApi) => {
@ -64,6 +62,6 @@ export const registerCommitCreate = (api: typeof changelogCommitApi) => {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json(CommitCreateOutput.parse(result), 201)
})
}

View File

@ -4,8 +4,10 @@ import { createRoute } from '@hono/zod-openapi'
import { and, eq, isNotNull, isNull } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import { P, match } from 'ts-pattern'
import type { changelogCommitApi } from '.'
import { verifyAuthentication } from '../../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../../utils/openapi'
const route = createRoute({
method: 'get',
@ -23,13 +25,9 @@ const route = createRoute({
},
description: 'Return version by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerCommitList = (api: typeof changelogCommitApi) => {
@ -80,6 +78,6 @@ export const registerCommitList = (api: typeof changelogCommitApi) => {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(commits, 200)
return c.json(CommitListOutput.parse(commits), 200)
})
}

View File

@ -22,6 +22,7 @@ export type Variables = {
export const app = new OpenAPIHono<{ Variables: Variables }>({
defaultHook: handleZodError,
strict: false,
})
// app.use(
@ -33,6 +34,14 @@ export const app = new OpenAPIHono<{ Variables: Variables }>({
app.onError(handleError)
app.use('*', cors())
app.use('/v1/*', authentication)
app.openAPIRegistry.registerComponent('securitySchemes', 'AccessToken', {
type: 'http',
scheme: 'bearer',
})
app.openAPIRegistry.registerComponent('securitySchemes', 'Clerk', {
type: 'http',
scheme: 'bearer',
})
app.route('/v1/user', user)
app.route('/v1/changelog', changelog)

View File

@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
import type { pageApi } from './index'
const route = createRoute({
@ -24,13 +25,9 @@ const route = createRoute({
},
description: 'Return changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerPageById = (api: typeof pageApi) => {
@ -60,6 +57,6 @@ export const registerPageById = (api: typeof pageApi) => {
changelogs: changelogs.map((log) => log.changelog),
}
return c.json(mappedResult, 200)
return c.json(PageOutput.parse(mappedResult), 200)
})
}

View File

@ -4,6 +4,7 @@ import { createRoute, type z } from '@hono/zod-openapi'
import { HTTPException } from 'hono/http-exception'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
import type { pageApi } from './index'
const route = createRoute({
@ -27,13 +28,9 @@ const route = createRoute({
},
description: 'Return changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerPageCreate = (api: typeof pageApi) => {
@ -42,6 +39,7 @@ export const registerPageCreate = (api: typeof pageApi) => {
const { changelogIds, ...rest }: z.infer<typeof PageCreateInput> =
await c.req.json()
const [result] = await db
.insert(page)
.values({
@ -50,6 +48,10 @@ export const registerPageCreate = (api: typeof pageApi) => {
})
.returning()
if (!result) {
throw new HTTPException(404, { message: 'Not Found' })
}
// TODO: implement transaction
if (changelogIds.length > 0) {
await db.insert(changelogs_to_pages).values(
@ -59,10 +61,7 @@ export const registerPageCreate = (api: typeof pageApi) => {
})),
)
}
if (!result) {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json(PageOutput.parse(result), 200)
})
}

View File

@ -3,8 +3,10 @@ import { GeneralOutput, PageByIdParams } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import type { pageApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
const route = createRoute({
method: 'delete',
@ -23,27 +25,25 @@ const route = createRoute({
},
description: 'Removes a changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerPageDelete = (api: typeof pageApi) => {
return api.openapi(route, async (c) => {
const userId = await verifyAuthentication(c)
const { id } = c.req.valid('param')
const result = await db
.delete(page)
.where(and(eq(page.userId, userId), eq(page.id, id)))
.returning()
if (!result) {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json({}, 200)
})
}

View File

@ -1,6 +1,7 @@
import { OpenAPIHono } from '@hono/zod-openapi'
import { timing } from 'hono/timing'
import type { Variables } from '..'
import { handleZodError } from '../utils/errors'
import type { ContextModule } from '../utils/sentry'
import { registerPageById } from './byId'
import { registerPageCreate } from './create'
@ -9,7 +10,9 @@ import { registerPageList } from './list'
import { registerPagePublic } from './public'
import { registerPageUpdate } from './update'
export const pageApi = new OpenAPIHono<{ Variables: Variables }>()
export const pageApi = new OpenAPIHono<{ Variables: Variables }>({
defaultHook: handleZodError,
})
pageApi.use('*', timing())
const module: ContextModule = {
name: 'page',

View File

@ -1,16 +1,17 @@
import { db, page } from '@boring.tools/database'
import { PageListOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm'
import { PageListOutput } from '@boring.tools/schema'
import { HTTPException } from 'hono/http-exception'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
import type { pageApi } from './index'
const route = createRoute({
method: 'get',
tags: ['page'],
description: 'Get a page list',
description: 'Get a list of pages',
path: '/',
responses: {
200: {
@ -21,13 +22,9 @@ const route = createRoute({
},
description: 'Return changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerPageList = (api: typeof pageApi) => {
@ -42,6 +39,6 @@ export const registerPageList = (api: typeof pageApi) => {
throw new HTTPException(404, { message: 'Not Found' })
}
return c.json(result, 200)
return c.json(PageListOutput.parse(result), 200)
})
}

View File

@ -1,18 +1,18 @@
import { changelog_version, db, page } from '@boring.tools/database'
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { eq } from 'drizzle-orm'
import { endTime, setMetric, startTime } from 'hono/timing'
import { PagePublicOutput, PagePublicParams } from '@boring.tools/schema'
import { HTTPException } from 'hono/http-exception'
import { endTime, startTime } from 'hono/timing'
import { openApiErrorResponses } from '../utils/openapi'
import { redis } from '../utils/redis'
import type { pageApi } from './index'
const route = createRoute({
method: 'get',
tags: ['page'],
description: 'Get a page',
description: 'Get a page by id for public view',
path: '/:id/public',
request: {
params: PagePublicParams,
@ -24,14 +24,9 @@ const route = createRoute({
schema: PagePublicOutput,
},
},
description: 'Return changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
description: 'Get a page by id for public view',
},
...openApiErrorResponses,
},
})
@ -96,6 +91,6 @@ export const registerPagePublic = (api: typeof pageApi) => {
}
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
return c.json(mappedResult, 200)
return c.json(PagePublicOutput.parse(mappedResult), 200)
})
}

View File

@ -9,6 +9,7 @@ import {
import { and, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
import { redis } from '../utils/redis'
import type { pageApi } from './index'
@ -34,13 +35,9 @@ const route = createRoute({
},
description: 'Return changelog by id',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerPageUpdate = (api: typeof pageApi) => {
@ -60,6 +57,10 @@ export const registerPageUpdate = (api: typeof pageApi) => {
.where(and(eq(page.userId, userId), eq(page.id, id)))
.returning()
if (!result) {
throw new HTTPException(404, { message: 'Not Found' })
}
// TODO: implement transaction
if (changelogIds) {
if (changelogIds.length === 0) {
@ -80,12 +81,8 @@ export const registerPageUpdate = (api: typeof pageApi) => {
}
}
if (!result) {
throw new HTTPException(404, { message: 'Not Found' })
}
redis.del(id)
return c.json(result, 200)
return c.json(PageOutput.parse(result), 200)
})
}

View File

@ -2,8 +2,10 @@ import { changelog, db, page } from '@boring.tools/database'
import { StatisticOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { eq } from 'drizzle-orm'
import type { statisticApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
const route = createRoute({
method: 'get',
@ -16,13 +18,9 @@ const route = createRoute({
},
description: 'Return user',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerStatisticGet = (api: typeof statisticApi) => {
@ -108,6 +106,6 @@ export const registerStatisticGet = (api: typeof statisticApi) => {
},
}
return c.json(mappedData, 200)
return c.json(StatisticOutput.parse(mappedData), 200)
})
}

View File

@ -2,7 +2,9 @@ import { db, user as userDb } from '@boring.tools/database'
import { UserOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { eq } from 'drizzle-orm'
import type { userApi } from '.'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
const route = createRoute({
method: 'get',
@ -15,13 +17,9 @@ const route = createRoute({
},
description: 'Return user',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
export const registerUserGet = (api: typeof userApi) => {
@ -35,6 +33,6 @@ export const registerUserGet = (api: typeof userApi) => {
throw new Error('User not found')
}
return c.json(result, 200)
return c.json(UserOutput.parse(result), 200)
})
}

View File

@ -4,7 +4,9 @@ import { UserOutput, UserWebhookInput } from '@boring.tools/schema'
import { createRoute, type z } from '@hono/zod-openapi'
import { HTTPException } from 'hono/http-exception'
import { Webhook } from 'svix'
import type userApi from '.'
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
const route = createRoute({
method: 'post',
@ -24,13 +26,9 @@ const route = createRoute({
},
description: 'Return success',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
...openApiErrorResponses,
},
...openApiSecurity,
})
const userCreate = async ({
@ -72,7 +70,7 @@ export const registerUserWebhook = (api: typeof userApi) => {
case 'user.created': {
const result = await userCreate({ payload: verifiedPayload })
logger.info('Clerk Webhook', result)
return c.json(result, 200)
return c.json(UserOutput.parse(result), 200)
}
default:
throw new HTTPException(404, { message: 'Webhook type not supported' })

View File

@ -0,0 +1,26 @@
import { type RouteConfig, createRoute as route } from '@hono/zod-openapi'
import { openApiErrorResponses } from './openapi'
type CreateRoute = {
method: 'get' | 'post' | 'put' | 'delete'
tags: string[]
description: string
path: string
responses: RouteConfig['responses']
request?: RouteConfig['request']
}
export const createRoute = (input: CreateRoute) =>
route({
...input,
responses: {
...input.responses,
...openApiErrorResponses,
},
security: [
{
Clerk: [],
AccessToken: [],
},
],
})

View File

@ -0,0 +1,68 @@
import { createErrorSchema } from './errors'
export const openApiSecurity = {
security: [
{
Clerk: [],
AccessToken: [],
},
],
}
export const openApiErrorResponses = {
400: {
description:
'The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).',
content: {
'application/json': {
schema: createErrorSchema('BAD_REQUEST').openapi('ErrBadRequest'),
},
},
},
401: {
description:
'The client must authenticate itself to get the requested response.',
content: {
'application/json': {
schema: createErrorSchema('UNAUTHORIZED').openapi('ErrUnauthorized'),
},
},
},
403: {
description:
'The client does not have the necessary permissions to access the resource.',
content: {
'application/json': {
schema: createErrorSchema('FORBIDDEN').openapi('ErrForbidden'),
},
},
},
404: {
description: "The server can't find the requested resource.",
content: {
'application/json': {
schema: createErrorSchema('NOT_FOUND').openapi('ErrNotFound'),
},
},
},
409: {
description:
'The request could not be completed due to a conflict mainly due to unique constraints.',
content: {
'application/json': {
schema: createErrorSchema('CONFLICT').openapi('ErrConflict'),
},
},
},
500: {
description:
"The server has encountered a situation it doesn't know how to handle.",
content: {
'application/json': {
schema: createErrorSchema('INTERNAL_SERVER_ERROR').openapi(
'ErrInternalServerError',
),
},
},
},
}

View File

@ -40,7 +40,7 @@ const Component = () => {
resolver: zodResolver(PageUpdateInput),
defaultValues: {
...page.data,
changelogIds: page.data?.changelogs.map((log) => log.id),
changelogIds: page.data?.changelogs?.map((log) => log.id),
},
})
const onSubmit = (values: z.infer<typeof PageUpdateInput>) => {
@ -138,6 +138,10 @@ const Component = () => {
key={changelog.id}
onSelect={() => {
const getIds = () => {
if (!field.value) {
return [changelog.id]
}
if (field.value?.includes(changelog.id)) {
return field.value.filter(
(id) => id !== changelog.id,

View File

@ -31,12 +31,14 @@ const Component = () => {
<Card className="w-full max-w-screen-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Changelogs ({data.changelogs?.length})</CardTitle>
<CardTitle>
Changelogs ({data.changelogs?.length ?? 0})
</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
{data.changelogs.map((changelog) => {
{data.changelogs?.map((changelog) => {
return (
<div className="flex gap-3" key={changelog.id}>
<Link

View File

@ -1 +1 @@
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/AccessToken/Delete.tsx","./src/components/AccessToken/Table/Columns.tsx","./src/components/AccessToken/Table/DataTable.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useAccessToken.ts","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/access-tokens.index.lazy.tsx","./src/routes/access-tokens.new.lazy.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/cli.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/AccessToken/Delete.tsx","./src/components/AccessToken/Table/Columns.tsx","./src/components/AccessToken/Table/DataTable.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/Step02.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useAccessToken.ts","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/hooks/useStatistic.ts","./src/routes/__root.tsx","./src/routes/access-tokens.index.lazy.tsx","./src/routes/access-tokens.new.lazy.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/cli.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}

View File

@ -30,9 +30,9 @@ const data: PageById = await response.json()
<p class="prose text-sm">{data.description}</p>
</div>
{data.changelogs.length >= 2 && <div class="flex flex-col">
{data.changelogs?.length >= 2 && <div class="flex flex-col">
<h2 class="prose prose-xl">Changelogs</h2>
{data.changelogs.map((changelog) => {
{data.changelogs?.map((changelog) => {
if (changelog.versions && changelog.versions?.length < 1) {
return null
}
@ -65,7 +65,7 @@ const data: PageById = await response.json()
</div>
})}
</div>}
{data.changelogs.length === 1 && <div>
{data.changelogs?.length === 1 && <div>
<h2 class="uppercase text-sm prose tracking-widest">Changelog</h2>
{data.changelogs.map((changelog) => {
if (changelog.versions && changelog.versions?.length < 1) {

View File

@ -17,6 +17,6 @@ auth:bearer {
body:json {
{
"changelogId": "d83fe688-3331-4e64-9af6-318f82e511d4",
"commitIds": ["01cc79df-6d16-4496-b9ba-b238d686efc4"]
"version": "0.1.0"
}
}

15
bruno/Page/By Id.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: By Id
type: http
seq: 1
}
get {
url: {{API_URL}}/page/:id/public
body: none
auth: none
}
params:path {
id: 74ab8978-e6d4-436e-98b8-fb6d6fde35fe
}

BIN
bt-cli Executable file

Binary file not shown.

BIN
bun.lockb

Binary file not shown.

View File

@ -12,7 +12,7 @@
"devDependencies": {
"@types/bun": "latest",
"@types/pg": "^8.11.10",
"drizzle-kit": "^0.27.1"
"drizzle-kit": "^0.28.0"
},
"peerDependencies": {
"typescript": "^5.0.0"

View File

@ -11,6 +11,7 @@
"dependencies": {
"@logtail/node": "^0.5.0",
"@logtail/winston": "^0.5.0",
"winston": "^3.14.2"
"winston": "^3.14.2",
"winston-loki": "^6.1.3"
}
}

View File

@ -1,6 +1,7 @@
import { Logtail } from '@logtail/node'
import { LogtailTransport } from '@logtail/winston'
import winston from 'winston'
import LokiTransport from 'winston-loki'
// Create a Winston logger - passing in the Logtail transport
export const logger = winston.createLogger({
@ -9,6 +10,14 @@ export const logger = winston.createLogger({
new winston.transports.Console({
format: winston.format.json(),
}),
new LokiTransport({
host: 'http://localhost:9100',
labels: { app: 'api' },
json: true,
format: winston.format.json(),
replaceTimestamp: true,
onConnectionError: (err) => console.error(err),
}),
],
})

View File

@ -7,6 +7,6 @@ export const AccessTokenOutput = z
}),
token: z.string().optional(),
name: z.string(),
lastUsedOn: z.string().or(z.date()),
lastUsedOn: z.string().or(z.date()).optional().nullable(),
})
.openapi('Access Token')

View File

@ -13,13 +13,15 @@ export const ChangelogOutput = z
example: 'This is a changelog',
}),
versions: z.array(VersionOutput).optional(),
computed: z.object({
versionCount: z.number().openapi({
example: 5,
}),
commitCount: z.number().openapi({
example: 10,
}),
}),
computed: z
.object({
versionCount: z.number().openapi({
example: 5,
}),
commitCount: z.number().openapi({
example: 10,
}),
})
.optional(),
})
.openapi('Changelog')

View File

@ -1,4 +1,5 @@
import { z } from '@hono/zod-openapi'
import { ChangelogOutput } from '../changelog'
export const PageOutput = z
.object({
@ -8,5 +9,6 @@ export const PageOutput = z
title: z.string(),
description: z.string().optional(),
icon: z.string(),
changelogs: z.array(ChangelogOutput).optional(),
})
.openapi('Page')

View File

@ -1,9 +1,4 @@
import { z } from '@hono/zod-openapi'
import { ChangelogOutput } from '../changelog'
import { PageOutput } from './base'
export const PageListOutput = z.array(
PageOutput.extend({
changelogs: z.array(ChangelogOutput),
}),
)
export const PageListOutput = z.array(PageOutput)