import type { GQLRequest, GQLResponse } from './types'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { All, Controller, Delete, Get, HttpException, HttpStatus, Inject, Query, Req, Res, } from '@nestjs/common'; import type { Cache } from 'cache-manager'; import { FastifyReply, FastifyRequest } from 'fastify'; import { env } from 'src/config/env'; import { extractDocumentId, getQueryType } from 'src/utils/query'; import { getQueryTTL } from './lib/utils'; type RedisStore = Omit & { set: (key: string, value: unknown, { ttl }: { ttl: number }) => Promise; }; @Controller('api') export class ProxyController { constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: RedisStore) {} @All('/graphql') public async graphql(@Req() req: FastifyRequest, @Res() reply: FastifyReply) { const { operationName, query, variables } = req.body as GQLRequest; const queryType = getQueryType(query); const key = `${operationName} ${JSON.stringify(variables)}`; if (queryType.action === 'query') { const cached = await this.cacheManager.get(key); if (cached) return reply.send(cached); } const response = await fetch(env.URL_GRAPHQL, { body: JSON.stringify({ operationName, query, variables }), headers: { Authorization: req.headers.authorization, 'Content-Type': 'application/json', Cookie: req.headers.cookie, }, method: req.method, }); const data = (await response.json()) as GQLResponse; if (!response.ok || data?.error || data?.errors?.length) throw new HttpException( response.statusText, response.status || HttpStatus.INTERNAL_SERVER_ERROR, ); if (queryType.action === 'mutation' && queryType.entity) { const documentId = extractDocumentId(data); const keys = await this.cacheManager.store.keys(`*${queryType.entity}*`); for (const key of keys) { if (key.includes(documentId)) { await this.cacheManager.del(key); // console.log(`🗑 Cache invalidated (by key): ${key}`); continue; } const value = await this.cacheManager.get(key); const serialized = typeof value === 'string' ? value : JSON.stringify(value); if (serialized?.includes(documentId)) { await this.cacheManager.del(key); // console.log(`🗑 Cache invalidated (by value): ${key}`); } } } const ttl = getQueryTTL(operationName); if (queryType.action === 'query' && data && ttl !== false) await this.cacheManager.set(key, data, { ttl }); return reply.send(data); } @Get('/get-queries') public async getQueriesList(@Res() reply: FastifyReply) { const keys: string[] = await this.cacheManager.store.keys('*'); const entries = await Promise.all( keys.map(async (key) => { try { const value = await this.cacheManager.get(key); return { key, value }; } catch (e) { return { key, error: e.message }; } }), ); return reply.send(entries); } @Delete('/delete-query') public async deleteQuery(@Query('queryKey') queryKey: string, @Res() reply: FastifyReply) { try { await this.cacheManager.del(queryKey); return reply.send('ok'); } catch (error) { throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } } @Delete('/reset') public async reset(@Res() reply: FastifyReply) { try { await this.cacheManager.reset(); return reply.send('ok'); } catch (error) { throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } } @Get('/get-query') public async getQueryValue(@Query('queryKey') queryKey: string, @Res() reply: FastifyReply) { try { const value = await this.cacheManager.get(queryKey); return reply.send(value); } catch (error) { throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } } }