feat(api): add changelog routes and tests
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m35s
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m35s
This commit is contained in:
parent
060bb71b0e
commit
0d0e241e68
53
apps/api/src/changelog/byId.ts
Normal file
53
apps/api/src/changelog/byId.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import { ChangelogByIdParams, ChangelogOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/:id',
|
||||
request: {
|
||||
params: ChangelogByIdParams,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ChangelogOutput,
|
||||
},
|
||||
},
|
||||
description: 'Return changelog by id',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||
const result = await db.query.changelog.findFirst({
|
||||
where: and(eq(changelog.userId, userId), eq(changelog.id, id)),
|
||||
with: {
|
||||
versions: {
|
||||
orderBy: (changelog_version, { desc }) => [
|
||||
desc(changelog_version.releasedAt),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
140
apps/api/src/changelog/changelog.test.ts
Normal file
140
apps/api/src/changelog/changelog.test.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
||||
import { access_token, db, user } from '@boring.tools/database'
|
||||
import type {
|
||||
AccessTokenOutput,
|
||||
ChangelogCreateInput,
|
||||
ChangelogCreateOutput,
|
||||
ChangelogListOutput,
|
||||
ChangelogOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import type { z } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { fetch } from '../utils/testing/fetch'
|
||||
|
||||
describe('Changelog', () => {
|
||||
let testAccessToken: AccessTokenOutput
|
||||
let testChangelog: z.infer<typeof ChangelogOutput>
|
||||
|
||||
beforeAll(async () => {
|
||||
await db
|
||||
.insert(user)
|
||||
.values({ email: 'changelog@test.local', id: 'test_000' })
|
||||
const tAccessToken = await db
|
||||
.insert(access_token)
|
||||
.values({ token: 'test123', userId: 'test_000', name: 'testtoken' })
|
||||
.returning()
|
||||
testAccessToken = tAccessToken[0]
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.delete(user).where(eq(user.email, 'changelog@test.local'))
|
||||
})
|
||||
|
||||
describe('Create', () => {
|
||||
test('Success', async () => {
|
||||
const payload: z.infer<typeof ChangelogCreateInput> = {
|
||||
title: 'changelog',
|
||||
description: 'description',
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/changelog',
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
const json: z.infer<typeof ChangelogCreateOutput> = await res.json()
|
||||
testChangelog = json
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(json.title).toBe(payload.title)
|
||||
})
|
||||
})
|
||||
|
||||
describe('By Id', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
test('Not Found', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
test('Invalid Id', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/changelog/some',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json.success).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: '/v1/changelog',
|
||||
method: 'GET',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json: z.infer<typeof ChangelogListOutput> = await res.json()
|
||||
expect(json).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove', () => {
|
||||
test('Success', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
})
|
||||
|
||||
test('Not found', async () => {
|
||||
const res = await fetch(
|
||||
{
|
||||
path: `/v1/changelog/${testChangelog.id}`,
|
||||
method: 'DELETE',
|
||||
},
|
||||
testAccessToken.token,
|
||||
)
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
})
|
53
apps/api/src/changelog/create.ts
Normal file
53
apps/api/src/changelog/create.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import {
|
||||
ChangelogCreateInput,
|
||||
ChangelogCreateOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'post',
|
||||
path: '/',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: ChangelogCreateInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': { schema: ChangelogCreateOutput },
|
||||
},
|
||||
description: 'Return created changelog',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const func = async ({
|
||||
userId,
|
||||
payload,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof ChangelogCreateInput>
|
||||
}) => {
|
||||
return await db
|
||||
.insert(changelog)
|
||||
.values({
|
||||
...payload,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
44
apps/api/src/changelog/delete.ts
Normal file
44
apps/api/src/changelog/delete.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import { GeneralOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'delete',
|
||||
path: '/:id',
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GeneralOutput,
|
||||
},
|
||||
},
|
||||
description: 'Removes a changelog by id',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const func = async ({ userId, id }: { userId: string; id: string }) => {
|
||||
const result = await db
|
||||
.delete(changelog)
|
||||
.where(and(eq(changelog.userId, userId), eq(changelog.id, id)))
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
107
apps/api/src/changelog/index.ts
Normal file
107
apps/api/src/changelog/index.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { OpenAPIHono } from '@hono/zod-openapi'
|
||||
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
import type { Variables } from '..'
|
||||
import { verifyAuthentication } from '../utils/authentication'
|
||||
import ById from './byId'
|
||||
import Create from './create'
|
||||
import Delete from './delete'
|
||||
import List from './list'
|
||||
import Update from './update'
|
||||
|
||||
const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
app.openapi(ById.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await ById.func({ userId, id })
|
||||
return c.json(result, 200)
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json({ message: error.message }, error.status)
|
||||
}
|
||||
return c.json({ message: 'An unexpected error occurred' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(List.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
try {
|
||||
const result = await List.func({ userId })
|
||||
return c.json(result, 200)
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json({ message: error.message }, error.status)
|
||||
}
|
||||
return c.json({ message: 'An unexpected error occurred' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Create.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const [result] = await Create.func({
|
||||
userId,
|
||||
payload: await c.req.json(),
|
||||
})
|
||||
return c.json(result, 201)
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json({ message: error.message }, error.status)
|
||||
}
|
||||
return c.json({ message: 'An unexpected error occurred' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Delete.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
const result = await Delete.func({ userId, id })
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
return c.json({ message: 'Changelog removed' })
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json({ message: error.message }, error.status)
|
||||
}
|
||||
return c.json({ message: 'An unexpected error occurred' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.openapi(Update.route, async (c) => {
|
||||
const userId = verifyAuthentication(c)
|
||||
|
||||
try {
|
||||
const id = c.req.param('id')
|
||||
|
||||
if (!id) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
const result = await Update.func({
|
||||
userId,
|
||||
payload: await c.req.json(),
|
||||
id,
|
||||
})
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ message: 'Changelog not found' }, 404)
|
||||
}
|
||||
|
||||
return c.json(result)
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json({ message: error.message }, error.status)
|
||||
}
|
||||
return c.json({ message: 'An unexpected error occurred' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
54
apps/api/src/changelog/list.ts
Normal file
54
apps/api/src/changelog/list.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import { ChangelogListOutput } from '@boring.tools/schema'
|
||||
import { createRoute } from '@hono/zod-openapi'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ChangelogListOutput,
|
||||
},
|
||||
},
|
||||
description: 'Return changelogs for current user',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const func = async ({ userId }: { userId: string }) => {
|
||||
const result = await db.query.changelog.findMany({
|
||||
where: eq(changelog.userId, userId),
|
||||
with: {
|
||||
versions: true,
|
||||
commits: {
|
||||
columns: { id: true },
|
||||
},
|
||||
},
|
||||
orderBy: (changelog, { asc }) => [asc(changelog.createdAt)],
|
||||
})
|
||||
|
||||
return result.map((changelog) => {
|
||||
const { versions, commits, ...rest } = changelog
|
||||
return {
|
||||
...rest,
|
||||
computed: {
|
||||
versionCount: versions.length,
|
||||
commitCount: commits.length,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
63
apps/api/src/changelog/update.ts
Normal file
63
apps/api/src/changelog/update.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { changelog, db } from '@boring.tools/database'
|
||||
import {
|
||||
ChangelogUpdateInput,
|
||||
ChangelogUpdateOutput,
|
||||
} from '@boring.tools/schema'
|
||||
import { createRoute, type z } from '@hono/zod-openapi'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
export const route = createRoute({
|
||||
method: 'put',
|
||||
path: '/:id',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': { schema: ChangelogUpdateInput },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': { schema: ChangelogUpdateOutput },
|
||||
},
|
||||
description: 'Return updated changelog',
|
||||
},
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const func = async ({
|
||||
userId,
|
||||
payload,
|
||||
id,
|
||||
}: {
|
||||
userId: string
|
||||
payload: z.infer<typeof ChangelogUpdateInput>
|
||||
id: string
|
||||
}) => {
|
||||
const result = await db
|
||||
.update(changelog)
|
||||
.set({
|
||||
...payload,
|
||||
})
|
||||
.where(and(eq(changelog.id, id), eq(changelog.userId, userId)))
|
||||
.returning()
|
||||
|
||||
if (!result) {
|
||||
throw new HTTPException(404, { message: 'Not found' })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
route,
|
||||
func,
|
||||
}
|
@ -3,6 +3,7 @@ import { OpenAPIHono, type z } from '@hono/zod-openapi'
|
||||
import { apiReference } from '@scalar/hono-api-reference'
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
import changelog from './changelog'
|
||||
import user from './user'
|
||||
|
||||
import { authentication } from './utils/authentication'
|
||||
@ -17,9 +18,10 @@ export type Variables = {
|
||||
export const app = new OpenAPIHono<{ Variables: Variables }>()
|
||||
|
||||
app.use('*', cors())
|
||||
app.use('/api/*', authentication)
|
||||
app.use('/v1/*', authentication)
|
||||
|
||||
app.route('/api/user', user)
|
||||
app.route('/v1/user', user)
|
||||
app.route('/v1/changelog', changelog)
|
||||
|
||||
app.doc('/openapi.json', {
|
||||
openapi: '3.0.0',
|
||||
|
@ -13,7 +13,7 @@ const generatedToken = async (c: Context, next: Next) => {
|
||||
|
||||
const token = authHeader.replace('Bearer ', '')
|
||||
|
||||
const accessTokenResult = await db.query.accessToken.findFirst({
|
||||
const accessTokenResult = await db.query.access_token.findFirst({
|
||||
where: eq(access_token.token, token),
|
||||
with: {
|
||||
user: true,
|
||||
|
30
apps/api/src/utils/testing/fetch.ts
Normal file
30
apps/api/src/utils/testing/fetch.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { app } from '../..'
|
||||
|
||||
export const fetch = async (
|
||||
{
|
||||
method,
|
||||
body,
|
||||
path,
|
||||
}: {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
body?: Record<string, unknown> | Record<string, unknown>[]
|
||||
path: string
|
||||
},
|
||||
token: string,
|
||||
) => {
|
||||
try {
|
||||
// const token = await authorization(user)
|
||||
const headers = new Headers({
|
||||
Authorization: `Bearer ${token}`,
|
||||
'content-type': 'application/json',
|
||||
})
|
||||
|
||||
return app.request(path, {
|
||||
method,
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
24
packages/schema/src/changelog/base.ts
Normal file
24
packages/schema/src/changelog/base.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export const ChangelogOutput = z
|
||||
.object({
|
||||
id: z.string().uuid().openapi({
|
||||
example: '9f00f912-f687-42ef-84d7-efde48ca11ef',
|
||||
}),
|
||||
title: z.string().min(3).openapi({
|
||||
example: 'My Changelog',
|
||||
}),
|
||||
description: z.string().openapi({
|
||||
example: 'This is a changelog',
|
||||
}),
|
||||
versions: z.array(z.object({})).optional(),
|
||||
computed: z.object({
|
||||
versionCount: z.number().openapi({
|
||||
example: 5,
|
||||
}),
|
||||
commitCount: z.number().openapi({
|
||||
example: 10,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.openapi('Changelog')
|
4
packages/schema/src/changelog/byId.ts
Normal file
4
packages/schema/src/changelog/byId.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { ChangelogOutput } from './base'
|
||||
|
||||
export const ChangelogListOutput = z.array(ChangelogOutput)
|
12
packages/schema/src/changelog/create.ts
Normal file
12
packages/schema/src/changelog/create.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { ChangelogOutput } from './base'
|
||||
|
||||
export const ChangelogCreateOutput = ChangelogOutput
|
||||
export const ChangelogCreateInput = z
|
||||
.object({
|
||||
title: z.string().min(3, 'Title must contain at least 3 charachters.'),
|
||||
description: z.string(),
|
||||
})
|
||||
.openapi({
|
||||
required: ['title', 'userId'],
|
||||
})
|
5
packages/schema/src/changelog/index.ts
Normal file
5
packages/schema/src/changelog/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './base'
|
||||
export * from './list'
|
||||
export * from './create'
|
||||
export * from './byId'
|
||||
export * from './update'
|
14
packages/schema/src/changelog/list.ts
Normal file
14
packages/schema/src/changelog/list.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export const ChangelogByIdParams = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.openapi({
|
||||
param: {
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
},
|
||||
example: 'a5ed5965-0506-44e6-aaec-0465ff9fe092',
|
||||
}),
|
||||
})
|
13
packages/schema/src/changelog/update.ts
Normal file
13
packages/schema/src/changelog/update.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
import { ChangelogOutput } from './base'
|
||||
import { ChangelogCreateInput } from './create'
|
||||
|
||||
export const ChangelogUpdateOutput = ChangelogOutput
|
||||
export const ChangelogUpdateInput = ChangelogCreateInput
|
||||
export const ChangelogUpdateParams = z
|
||||
.object({
|
||||
id: z.string().uuid(),
|
||||
})
|
||||
.openapi({
|
||||
required: ['id'],
|
||||
})
|
9
packages/schema/src/general/base.ts
Normal file
9
packages/schema/src/general/base.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
export const GeneralOutput = z
|
||||
.object({
|
||||
message: z.string().optional().openapi({
|
||||
example: 'Something',
|
||||
}),
|
||||
})
|
||||
.openapi('General')
|
1
packages/schema/src/general/index.ts
Normal file
1
packages/schema/src/general/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './base'
|
@ -1 +1,3 @@
|
||||
export * from './general'
|
||||
export * from './user'
|
||||
export * from './changelog'
|
||||
|
Loading…
Reference in New Issue
Block a user