From 106b3c05458f7ffa5b38bc3fe83451492ca8ed4b Mon Sep 17 00:00:00 2001 From: Lars Hampe Date: Sat, 26 Oct 2024 14:30:15 +0200 Subject: [PATCH] feat: cli commit upload --- apps/api/.env.example | 1 - apps/api/package.json | 3 +- apps/api/src/changelog/commit/byId.ts | 66 -------------- apps/api/src/changelog/commit/create.ts | 11 ++- apps/api/src/changelog/commit/delete.ts | 63 ------------- apps/api/src/changelog/commit/index.ts | 115 +----------------------- apps/api/src/changelog/commit/list.ts | 84 +++++++++++++++++ apps/api/src/changelog/commit/update.ts | 72 --------------- apps/api/src/changelog/index.ts | 5 +- apps/api/src/index.ts | 4 +- apps/api/src/utils/authentication.ts | 2 + apps/api/src/utils/errors/index.ts | 1 + apps/cli/src/index.ts | 68 ++------------ apps/cli/src/upload_commits.ts | 49 ++++++++++ apps/cli/src/utils/arguments.ts | 42 +++++++++ apps/cli/src/utils/fetch_api.ts | 27 ++++++ apps/cli/src/utils/git_log.ts | 18 ++-- 17 files changed, 238 insertions(+), 393 deletions(-) delete mode 100644 apps/api/.env.example delete mode 100644 apps/api/src/changelog/commit/byId.ts delete mode 100644 apps/api/src/changelog/commit/delete.ts create mode 100644 apps/api/src/changelog/commit/list.ts delete mode 100644 apps/api/src/changelog/commit/update.ts create mode 100644 apps/cli/src/upload_commits.ts create mode 100644 apps/cli/src/utils/arguments.ts create mode 100644 apps/cli/src/utils/fetch_api.ts diff --git a/apps/api/.env.example b/apps/api/.env.example deleted file mode 100644 index b0ace5b..0000000 --- a/apps/api/.env.example +++ /dev/null @@ -1 +0,0 @@ -POSTGRES_URL=postgres://postgres:postgres@localhost:5432/postgres \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index f6e7086..c1a5f81 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,7 +15,8 @@ "@scalar/hono-api-reference": "^0.5.149", "hono": "^4.6.3", "semver": "^7.6.3", - "svix": "^1.36.0" + "svix": "^1.36.0", + "ts-pattern": "^5.5.0" }, "devDependencies": { "@types/bun": "latest", diff --git a/apps/api/src/changelog/commit/byId.ts b/apps/api/src/changelog/commit/byId.ts deleted file mode 100644 index ef8351f..0000000 --- a/apps/api/src/changelog/commit/byId.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { changelog, changelog_version, db } from '@boring.tools/database' -import { VersionByIdParams, VersionOutput } from '@boring.tools/schema' -import { createRoute } from '@hono/zod-openapi' -import { and, eq } from 'drizzle-orm' - -export const byId = createRoute({ - method: 'get', - path: '/:id', - request: { - params: VersionByIdParams, - }, - responses: { - 200: { - content: { - 'application/json': { - schema: VersionOutput, - }, - }, - description: 'Return version by id', - }, - 400: { - description: 'Bad Request', - }, - 500: { - description: 'Internal Server Error', - }, - }, -}) - -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, - }, - }) - - 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/commit/create.ts b/apps/api/src/changelog/commit/create.ts index c52d515..3b05a55 100644 --- a/apps/api/src/changelog/commit/create.ts +++ b/apps/api/src/changelog/commit/create.ts @@ -32,7 +32,7 @@ export const route = createRoute({ }, }) -export const regsiterCommitCreate = (api: typeof changelogCommitApi) => { +export const registerCommitCreate = (api: typeof changelogCommitApi) => { return api.openapi(route, async (c) => { const userId = verifyAuthentication(c) @@ -49,7 +49,14 @@ export const regsiterCommitCreate = (api: typeof changelogCommitApi) => { throw new HTTPException(404, { message: 'Not Found' }) } - const [result] = await db.insert(changelog_commit).values(data).returning() + const mappedData = data.map((entry) => ({ + ...entry, + createdAt: new Date(entry.author.date), + })) + const [result] = await db + .insert(changelog_commit) + .values(mappedData) + .returning() if (!result) { throw new HTTPException(404, { message: 'Not Found' }) diff --git a/apps/api/src/changelog/commit/delete.ts b/apps/api/src/changelog/commit/delete.ts deleted file mode 100644 index bd9f703..0000000 --- a/apps/api/src/changelog/commit/delete.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { changelog, changelog_version, 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 remove = createRoute({ - method: 'delete', - path: '/:id', - responses: { - 200: { - content: { - 'application/json': { - schema: GeneralOutput, - }, - }, - description: 'Removes a version by id', - }, - 404: { - content: { - 'application/json': { - schema: GeneralOutput, - }, - }, - description: 'Version not found', - }, - 500: { - description: 'Internal Server Error', - }, - }, -}) - -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), - }, - }, - }) - - const findChangelog = changelogResult.find((change) => - change.versions.find((ver) => ver.id === id), - ) - - if (!findChangelog?.versions.length) { - throw new HTTPException(404, { - message: 'Version not found', - }) - } - - return db - .delete(changelog_version) - .where(and(eq(changelog_version.id, id))) - .returning() -} diff --git a/apps/api/src/changelog/commit/index.ts b/apps/api/src/changelog/commit/index.ts index aab70ed..17884bb 100644 --- a/apps/api/src/changelog/commit/index.ts +++ b/apps/api/src/changelog/commit/index.ts @@ -1,7 +1,8 @@ import { OpenAPIHono } from '@hono/zod-openapi' import type { Variables } from '../..' import type { ContextModule } from '../../utils/sentry' -import { regsiterCommitCreate } from './create' +import { registerCommitCreate } from './create' +import { registerCommitList } from './list' export const changelogCommitApi = new OpenAPIHono<{ Variables: Variables }>() @@ -10,113 +11,5 @@ const module: ContextModule = { sub_module: 'version', } -regsiterCommitCreate(changelogCommitApi) - -// app.openapi(create, async (c) => { -// const userId = 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, -// }, -// }) -// } -// }) - -/* app.openapi(byId, async (c) => { - const userId = 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, - }, - }) - } -}) - -app.openapi(update, async (c) => { - const userId = 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, - }, - }) - } -}) - -app.openapi(remove, async (c) => { - const userId = 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, - }, - }) - } -}) */ +registerCommitCreate(changelogCommitApi) +registerCommitList(changelogCommitApi) diff --git a/apps/api/src/changelog/commit/list.ts b/apps/api/src/changelog/commit/list.ts new file mode 100644 index 0000000..2c195eb --- /dev/null +++ b/apps/api/src/changelog/commit/list.ts @@ -0,0 +1,84 @@ +import { changelog, changelog_commit, db } from '@boring.tools/database' +import { CommitListOutput, CommitListParams } from '@boring.tools/schema' +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' + +const route = createRoute({ + method: 'get', + path: '/', + request: { + query: CommitListParams, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: CommitListOutput, + }, + }, + description: 'Return version by id', + }, + 400: { + description: 'Bad Request', + }, + 500: { + description: 'Internal Server Error', + }, + }, +}) + +export const registerCommitList = (api: typeof changelogCommitApi) => { + return api.openapi(route, async (c) => { + const userId = verifyAuthentication(c) + const { changelogId, limit, offset, hasVersion } = c.req.valid('query') + const result = await db.query.changelog.findFirst({ + where: and(eq(changelog.userId, userId), eq(changelog.id, changelogId)), + }) + + if (!result) { + throw new HTTPException(404, { message: 'Changelog not found' }) + } + + const where = match({ changelogId, hasVersion }) + .with( + { + changelogId: P.select('changelogId'), + hasVersion: P.when((hasVersion) => hasVersion === true), + }, + ({ changelogId }) => + and( + eq(changelog_commit.changelogId, changelogId), + isNotNull(changelog_commit.versionId), + ), + ) + .with( + { + changelogId: P.select('changelogId'), + hasVersion: P.when((hasVersion) => hasVersion === false), + }, + ({ changelogId }) => + and( + eq(changelog_commit.changelogId, changelogId), + isNull(changelog_commit.versionId), + ), + ) + .otherwise(() => eq(changelog_commit.changelogId, changelogId)) + + const commits = await db.query.changelog_commit.findMany({ + where, + limit: Number(limit) ?? 10, + offset: Number(offset) ?? 0, + orderBy: (_, { desc }) => [desc(changelog_commit.createdAt)], + }) + + if (!commits) { + throw new HTTPException(404, { message: 'Not Found' }) + } + + return c.json(commits, 200) + }) +} diff --git a/apps/api/src/changelog/commit/update.ts b/apps/api/src/changelog/commit/update.ts deleted file mode 100644 index 8ce04d5..0000000 --- a/apps/api/src/changelog/commit/update.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { changelog, changelog_version, db } from '@boring.tools/database' -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' - -export const update = createRoute({ - method: 'put', - path: '/:id', - request: { - body: { - content: { - 'application/json': { schema: VersionUpdateInput }, - }, - }, - }, - responses: { - 200: { - content: { - 'application/json': { schema: VersionUpdateOutput }, - }, - description: 'Return updated version', - }, - 400: { - description: 'Bad Request', - }, - 500: { - description: 'Internal Server Error', - }, - }, -}) - -export const updateFunc = async ({ - userId, - payload, - id, -}: { - userId: string - payload: z.infer - id: string -}) => { - 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', - }) - } - - const [versionUpdateResult] = await db - .update(changelog_version) - .set({ - status: payload.status, - markdown: payload.markdown, - releasedAt: payload.releasedAt ? new Date(payload.releasedAt) : null, - }) - .where(and(eq(changelog_version.id, id))) - .returning() - - return versionUpdateResult -} diff --git a/apps/api/src/changelog/index.ts b/apps/api/src/changelog/index.ts index ccfb1d6..38413ca 100644 --- a/apps/api/src/changelog/index.ts +++ b/apps/api/src/changelog/index.ts @@ -3,17 +3,20 @@ import type { Variables } from '..' import { verifyAuthentication } from '../utils/authentication' import { type ContextModule, captureSentry } from '../utils/sentry' import ById from './byId' +import { changelogCommitApi } from './commit' import Create from './create' import Delete from './delete' import List from './list' import Update from './update' +import version from './version' const app = new OpenAPIHono<{ Variables: Variables }>() const module: ContextModule = { name: 'changelog', } - +app.route('/commit', changelogCommitApi) +app.route('/version', version) app.openapi(ById.route, async (c) => { const userId = verifyAuthentication(c) try { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index efae23c..e2d2eb4 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -21,7 +21,7 @@ export type Variables = { } export const app = new OpenAPIHono<{ Variables: Variables }>({ - defaultHook: handleZodError, + // defaultHook: handleZodError, }) // app.use( @@ -36,8 +36,6 @@ app.use('/v1/*', authentication) app.route('/v1/user', user) app.route('/v1/changelog', changelog) -app.route('/v1/changelog/version', version) -app.route('/v1/changelog/commit', changelogCommitApi) app.route('/v1/page', pageApi) app.doc('/openapi.json', { diff --git a/apps/api/src/utils/authentication.ts b/apps/api/src/utils/authentication.ts index 23ac344..0b8bba0 100644 --- a/apps/api/src/utils/authentication.ts +++ b/apps/api/src/utils/authentication.ts @@ -20,6 +20,8 @@ const generatedToken = async (c: Context, next: Next) => { }, }) + console.log(accessTokenResult) + if (!accessTokenResult) { throw new HTTPException(401, { message: 'Unauthorized' }) } diff --git a/apps/api/src/utils/errors/index.ts b/apps/api/src/utils/errors/index.ts index f86c0c3..ce3ff75 100644 --- a/apps/api/src/utils/errors/index.ts +++ b/apps/api/src/utils/errors/index.ts @@ -106,6 +106,7 @@ export function handleZodError( }, c: Context, ) { + console.log('asdasasdas') if (!result.success) { const error = SchemaError.fromZod(result.error, c) return c.json>>( diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 95268e7..0625337 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -2,69 +2,11 @@ import { parseArgs } from 'node:util' import semver from 'semver' import { z } from 'zod' -import { git_log } from './utils/git_log' +import { upload_commits } from './upload_commits' //import { pushCommits } from './pushCommits' +import { args } from './utils/arguments' +import { git_log } from './utils/git_log' -const ENV_VERSION = Bun.env.CHANGELOG_VERSION -const ENV_ID = Bun.env.CHANGELOG_ID -const ENV_TOKEN = Bun.env.CHANGELOG_TOKEN - -const schema = z.object({ - title: z.string().optional(), - version: z.string().optional(), - token: z.string(), - changelogId: z.string(), -}) -export type Arguments = z.infer - -const { values } = parseArgs({ - args: Bun.argv, - options: { - title: { - type: 'string', - }, - version: { - type: 'string', - }, - token: { - type: 'string', - }, - changelogId: { - type: 'string', - }, - }, - strict: true, - allowPositionals: true, -}) - -const mappedArguments = { - ...values, - version: values.version || ENV_VERSION, - token: values.token || ENV_TOKEN, - changelogId: values.changelogId || ENV_ID, +if (args.success) { + upload_commits(args.data) } - -const args = schema.safeParse(mappedArguments) - -/*if (!args.success) { - console.error( - `@changelog/cli: Missing arguemnts: ${args.error.errors - .map((error) => error.path[0]) - .join(', ')}`, - ) - process.exit(1) -} */ - -// const version = semver.coerce(result.data.version); - -// // Check for correct semver -// if (!version) { -// console.error( -// "@changelog/cli: Invalid version. Please provide a valid semver version." -// ); -// process.exit(1); -// } - -//pushCommits(args.data) - -git_log('') diff --git a/apps/cli/src/upload_commits.ts b/apps/cli/src/upload_commits.ts new file mode 100644 index 0000000..af9853d --- /dev/null +++ b/apps/cli/src/upload_commits.ts @@ -0,0 +1,49 @@ +import type { Arguments } from './utils/arguments' +import { fetchAPI } from './utils/fetch_api' +import { git_log } from './utils/git_log' + +const getLastCommitHash = async (args: Arguments) => { + const result = await fetchAPI( + `/v1/changelog/commit?changelogId=${args.changelogId}&limit=1`, + {}, + args.token, + ) + + if (!result) { + return '' + } + + if (Array.isArray(result)) { + if (result.length === 0) { + return '' + } + return result[0].commit + } + + return '' +} + +export const upload_commits = async (arguemnts: Arguments) => { + const hash = await getLastCommitHash(arguemnts) + const commits = await git_log(hash) + + if (commits.length === 0) { + console.info('No new commits found') + return + } + + console.info(`Pushing ${commits.length} commits`) + const mappedCommits = commits.map((commit) => ({ + ...commit, + changelogId: arguemnts.changelogId, + })) + + await fetchAPI( + '/v1/changelog/commit', + { + method: 'POST', + body: JSON.stringify(mappedCommits), + }, + arguemnts.token, + ) +} diff --git a/apps/cli/src/utils/arguments.ts b/apps/cli/src/utils/arguments.ts new file mode 100644 index 0000000..9eb021c --- /dev/null +++ b/apps/cli/src/utils/arguments.ts @@ -0,0 +1,42 @@ +import { parseArgs } from 'node:util' +import { z } from 'zod' + +const ENV_ID = Bun.env.CHANGELOG_ID +const ENV_TOKEN = Bun.env.AUTH_TOKEN + +const schema = z.object({ + token: z.string(), + changelogId: z.string(), +}) +export type Arguments = z.infer + +const { values } = parseArgs({ + args: Bun.argv, + options: { + token: { + type: 'string', + }, + changelogId: { + type: 'string', + }, + }, + strict: true, + allowPositionals: true, +}) + +const mappedArguments = { + ...values, + token: values.token || ENV_TOKEN, + changelogId: values.changelogId || ENV_ID, +} + +export const args = schema.safeParse(mappedArguments) + +if (!args.success) { + console.error( + `@changelog/cli: Missing arguemnts: ${args.error.errors + .map((error) => error.path[0]) + .join(', ')}`, + ) + process.exit(1) +} diff --git a/apps/cli/src/utils/fetch_api.ts b/apps/cli/src/utils/fetch_api.ts new file mode 100644 index 0000000..847dd60 --- /dev/null +++ b/apps/cli/src/utils/fetch_api.ts @@ -0,0 +1,27 @@ +const getAPIUrl = () => { + if (Bun.env.NODE_ENV === 'development') { + return 'http://localhost:3000' + } + + return 'https://api.boring.tools' +} + +export const fetchAPI = async ( + url: string, + options: RequestInit, + token: string, +) => { + const response = await fetch(`${getAPIUrl()}${url}`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + ...options, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return response.json() +} diff --git a/apps/cli/src/utils/git_log.ts b/apps/cli/src/utils/git_log.ts index 44621e8..504c75f 100644 --- a/apps/cli/src/utils/git_log.ts +++ b/apps/cli/src/utils/git_log.ts @@ -1,21 +1,19 @@ -const GITFORMAT = `--pretty=format:{%n "commit": "%h",%n "parent": "%p",%n "refs": "%D",%n "subject": "%s",%n "notes": "%N",%n "body": "%b",%n "author": { "name": "%aN", "email": "%aE", "date": "%ad" },%n "commiter": { "name": "%cN", "email": "%cE", "date": "%cd" }%n},` +const GITFORMAT = + '--pretty=format:{"commit": "%h", "parent": "%p", "refs": "%D", "subject": "%s", "author": { "name": "%aN", "email": "%aE", "date": "%ad" }, "commiter": { "name": "%cN", "email": "%cE", "date": "%cd" }},' export const git_log = async ( from: string | undefined, to = 'HEAD', -): Promise => { +): Promise>> => { // https://git-scm.com/docs/pretty-formats - const r = await Bun.spawn([ + const process = Bun.spawn([ 'git', - '--no-pager', 'log', `${from ? `${from}...` : ''}${to}`, GITFORMAT, - //'--name-status', '--date=iso', ]) - const text = await new Response(r.stdout).text() - // console.log(JSON.parse('[' + text.slice(0, -1) + ']')) - // const str = JSON.stringify(`[${text.slice(0, -1)}]`) - // console.log(JSON.parse(str)) - return false + + const output = await new Response(process.stdout).text() + const jsonString = `[${output.slice(0, -1)}]` + return JSON.parse(jsonString) }