diff --git a/apps/api/package.json b/apps/api/package.json index 2c71e46..6925744 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,6 +15,7 @@ "@scalar/hono-api-reference": "^0.5.149", "convert-gitmoji": "^0.1.5", "hono": "^4.6.3", + "redis": "^4.7.0", "semver": "^7.6.3", "svix": "^1.36.0", "ts-pattern": "^5.5.0" diff --git a/apps/api/src/changelog/update.ts b/apps/api/src/changelog/update.ts index 74c7212..c757fee 100644 --- a/apps/api/src/changelog/update.ts +++ b/apps/api/src/changelog/update.ts @@ -6,6 +6,7 @@ import { import { createRoute, type z } from '@hono/zod-openapi' import { and, eq } from 'drizzle-orm' import { HTTPException } from 'hono/http-exception' +import { redis } from '../utils/redis' export const route = createRoute({ method: 'put', @@ -55,6 +56,9 @@ export const func = async ({ throw new HTTPException(404, { message: 'Not found' }) } + if (result.pageId) { + redis.del(result.pageId) + } return result } diff --git a/apps/api/src/changelog/version/create.ts b/apps/api/src/changelog/version/create.ts index ef96cde..36f15cd 100644 --- a/apps/api/src/changelog/version/create.ts +++ b/apps/api/src/changelog/version/create.ts @@ -9,6 +9,7 @@ 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 { redis } from '../../utils/redis' export const create = createRoute({ method: 'post', @@ -90,6 +91,10 @@ export const createFunc = async ({ }) .returning() + if (changelogResult.pageId) { + redis.del(changelogResult.pageId) + } + await db .update(changelog_commit) .set({ versionId: versionCreateResult.id }) diff --git a/apps/api/src/changelog/version/createAuto.ts b/apps/api/src/changelog/version/createAuto.ts index 4b3b0b1..592c74d 100644 --- a/apps/api/src/changelog/version/createAuto.ts +++ b/apps/api/src/changelog/version/createAuto.ts @@ -16,6 +16,7 @@ import semver from 'semver' import type { changelogVersionApi } from '.' import { verifyAuthentication } from '../../utils/authentication' import { commitsToMarkdown } from '../../utils/git/commitsToMarkdown' +import { redis } from '../../utils/redis' export const route = createRoute({ method: 'post', @@ -141,6 +142,10 @@ export const registerVersionCreateAuto = (api: typeof changelogVersionApi) => { .set({ versionId: versionCreateResult.id }) .where(isNull(changelog_commit.versionId)) + if (changelogResult.pageId) { + redis.del(changelogResult.pageId) + } + return c.json(versionCreateResult, 201) } @@ -164,6 +169,10 @@ export const registerVersionCreateAuto = (api: typeof changelogVersionApi) => { .set({ versionId: versionCreateResult.id }) .where(isNull(changelog_commit.versionId)) + if (changelogResult.pageId) { + redis.del(changelogResult.pageId) + } + return c.json(versionCreateResult, 201) }) } diff --git a/apps/api/src/changelog/version/delete.ts b/apps/api/src/changelog/version/delete.ts index 73d1850..a7cbad8 100644 --- a/apps/api/src/changelog/version/delete.ts +++ b/apps/api/src/changelog/version/delete.ts @@ -8,6 +8,7 @@ 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 { redis } from '../../utils/redis' export const remove = createRoute({ method: 'delete', @@ -67,6 +68,10 @@ export const removeFunc = async ({ .set({ versionId: null }) .where(eq(changelog_commit.versionId, id)) + if (findChangelog.pageId) { + redis.del(findChangelog.pageId) + } + return db .delete(changelog_version) .where(and(eq(changelog_version.id, id))) diff --git a/apps/api/src/changelog/version/update.ts b/apps/api/src/changelog/version/update.ts index e7f7d73..4d3501d 100644 --- a/apps/api/src/changelog/version/update.ts +++ b/apps/api/src/changelog/version/update.ts @@ -3,6 +3,7 @@ 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 { redis } from '../../utils/redis' export const update = createRoute({ method: 'put', @@ -70,5 +71,9 @@ export const updateFunc = async ({ .where(and(eq(changelog_version.id, id))) .returning() + if (findChangelog.pageId) { + redis.del(findChangelog.pageId) + } + return versionUpdateResult } diff --git a/apps/api/src/page/index.ts b/apps/api/src/page/index.ts index d22fda4..2b7c135 100644 --- a/apps/api/src/page/index.ts +++ b/apps/api/src/page/index.ts @@ -1,4 +1,5 @@ import { OpenAPIHono } from '@hono/zod-openapi' +import { timing } from 'hono/timing' import type { Variables } from '..' import type { ContextModule } from '../utils/sentry' import { registerPageById } from './byId' @@ -9,7 +10,7 @@ import { registerPagePublic } from './public' import { registerPageUpdate } from './update' export const pageApi = new OpenAPIHono<{ Variables: Variables }>() - +pageApi.use('*', timing()) const module: ContextModule = { name: 'page', } diff --git a/apps/api/src/page/public.ts b/apps/api/src/page/public.ts index acf7a2d..761abc1 100644 --- a/apps/api/src/page/public.ts +++ b/apps/api/src/page/public.ts @@ -1,9 +1,12 @@ import { changelog_version, db, page } from '@boring.tools/database' 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 { redis } from '../utils/redis' import type { pageApi } from './index' const route = createRoute({ @@ -35,6 +38,15 @@ const route = createRoute({ export const registerPagePublic = (api: typeof pageApi) => { return api.openapi(route, async (c) => { const { id } = c.req.valid('param') + const cache = await redis.get(id) + + if (cache) { + c.header('Cache-Control', 'public, max-age=86400') + c.header('X-Cache', 'HIT') + return c.json(JSON.parse(cache), 200) + } + + startTime(c, 'database') const result = await db.query.page.findFirst({ where: eq(page.id, id), @@ -70,6 +82,8 @@ export const registerPagePublic = (api: typeof pageApi) => { }, }) + endTime(c, 'database') + if (!result) { throw new HTTPException(404, { message: 'Not Found' }) } @@ -81,6 +95,7 @@ export const registerPagePublic = (api: typeof pageApi) => { changelogs: changelogs.map((log) => log.changelog), } + redis.set(id, JSON.stringify(mappedResult), { EX: 60 }) return c.json(mappedResult, 200) }) } diff --git a/apps/api/src/page/update.ts b/apps/api/src/page/update.ts index 8f8920e..8a467b8 100644 --- a/apps/api/src/page/update.ts +++ b/apps/api/src/page/update.ts @@ -9,6 +9,7 @@ import { import { and, eq } from 'drizzle-orm' import { HTTPException } from 'hono/http-exception' import { verifyAuthentication } from '../utils/authentication' +import { redis } from '../utils/redis' import type { pageApi } from './index' const route = createRoute({ @@ -83,6 +84,8 @@ export const registerPageUpdate = (api: typeof pageApi) => { throw new HTTPException(404, { message: 'Not Found' }) } + redis.del(id) + return c.json(result, 200) }) } diff --git a/apps/api/src/utils/redis.ts b/apps/api/src/utils/redis.ts new file mode 100644 index 0000000..6bb2c2e --- /dev/null +++ b/apps/api/src/utils/redis.ts @@ -0,0 +1,9 @@ +import { createClient } from 'redis' + +export const redis = createClient({ + password: import.meta.env.REDIS_PASSWORD, + url: import.meta.env.REDIS_URL, +}) + +redis.on('error', (err) => console.log('Redis Client Error', err)) +await redis.connect() diff --git a/apps/api/src/utils/startup.ts b/apps/api/src/utils/startup.ts index 49f901e..84de84d 100644 --- a/apps/api/src/utils/startup.ts +++ b/apps/api/src/utils/startup.ts @@ -5,6 +5,8 @@ import { logger } from '@boring.tools/logger' declare module 'bun' { interface Env { POSTGRES_URL: string + REDIS_PASSWORD: string + REDIS_URL: string CLERK_WEBHOOK_SECRET: string CLERK_SECRET_KEY: string CLERK_PUBLISHABLE_KEY: string @@ -12,7 +14,7 @@ declare module 'bun' { } } -const TEST_VARIABLES = ['POSTGRES_URL'] +const TEST_VARIABLES = ['POSTGRES_URL', 'REDIS_URL', 'REDIS_PASSWORD'] const DEVELOPMENT_VARIABLES = [ ...TEST_VARIABLES, diff --git a/bun.lockb b/bun.lockb index 2e7f3dc..3edda30 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yaml b/docker-compose.yaml index 78faec2..e763897 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,4 +8,51 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - POSTGRES_DB=postgres - \ No newline at end of file + + cache: + container_name: boring_redis + image: redis:7.4.1-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning --requirepass development + + loki: + image: grafana/loki:latest + ports: + - "9100:3100" + command: -config.file=/etc/loki/local-config.yaml + + promtail: + image: grafana/promtail:latest + volumes: + - /var/log:/var/log + command: -config.file=/etc/promtail/config.yml + + grafana: + environment: + - GF_PATHS_PROVISIONING=/etc/grafana/provisioning + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + entrypoint: + - sh + - -euc + - | + mkdir -p /etc/grafana/provisioning/datasources + cat < /etc/grafana/provisioning/datasources/ds.yaml + apiVersion: 1 + datasources: + - name: Loki + type: loki + access: proxy + orgId: 1 + url: http://loki:3100 + basicAuth: false + isDefault: true + version: 1 + editable: false + EOF + /run.sh + image: grafana/grafana:latest + ports: + - "9000:3000" \ No newline at end of file