feat(api): add redis cache for public page
This commit is contained in:
		
							parent
							
								
									52829bd127
								
							
						
					
					
						commit
						8657228e02
					
				| @ -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" | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 }) | ||||
|  | ||||
| @ -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) | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @ -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))) | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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', | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|   }) | ||||
| } | ||||
|  | ||||
							
								
								
									
										9
									
								
								apps/api/src/utils/redis.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/api/src/utils/redis.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
| @ -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, | ||||
|  | ||||
| @ -8,4 +8,51 @@ services: | ||||
|       - POSTGRES_PASSWORD=postgres | ||||
|       - POSTGRES_USER=postgres | ||||
|       - POSTGRES_DB=postgres | ||||
|    | ||||
| 
 | ||||
|   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 <<EOF > /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" | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user