Merged in feature/redis (pull request #1478)

Distributed cache

* cache deleteKey now uses an options object instead of a lonely argument variable fuzzy

* merge

* remove debug logs and cleanup

* cleanup

* add fault handling

* add fault handling

* add pid when logging redis client creation

* add identifier when logging redis client creation

* cleanup

* feat: add redis-api as it's own app

* feature: use http wrapper for redis

* feat: add the possibility to fallback to unstable_cache

* Add error handling if redis cache is unresponsive

* add logging for unstable_cache

* merge

* don't cache errors

* fix: metadatabase on branchdeploys

* Handle when /en/destinations throws
add ErrorBoundary

* Add sentry-logging when ErrorBoundary catches exception

* Fix error handling for distributed cache

* cleanup code

* Added Application Insights back

* Update generateApiKeys script and remove duplicate

* Merge branch 'feature/redis' of bitbucket.org:scandic-swap/web into feature/redis

* merge


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-03-14 07:54:21 +00:00
committed by Linus Flood
parent a8304e543e
commit fa63b20ed0
141 changed files with 4404 additions and 1941 deletions

View File

@@ -0,0 +1,93 @@
import { Elysia, t, ValidationError } from "elysia";
import { redis } from "@/services/redis";
import { ModelValidationError } from "@/errors/ModelValidationError";
const MIN_LENGTH = 1;
const QUERY_TYPE = t.Object({ key: t.String({ minLength: MIN_LENGTH }) });
export const cacheRoutes = new Elysia({ prefix: "/cache" })
.get(
"/",
async ({ query: { key }, error }) => {
key = validateKey(key);
console.log("GET /cache", key);
const value = await redis.get(key);
if (!value) {
return error("Not Found", "Not Found");
}
try {
const output = JSON.parse(value);
return { data: output };
} catch (e) {
redis.del(key);
throw e;
}
},
{
query: QUERY_TYPE,
response: { 200: t.Object({ data: t.Any() }), 404: t.String() },
}
)
.put(
"/",
async ({ query: { key }, body, error, set }) => {
key = validateKey(key);
console.log("PUT /cache", key);
if (!body.ttl || body.ttl < 0) {
return error("Bad Request", "ttl is required");
}
await redis.set(key, JSON.stringify(body.data), "EX", body.ttl);
set.status = 204;
return;
},
{
body: t.Object({ data: t.Any(), ttl: t.Number() }),
query: QUERY_TYPE,
response: { 204: t.Void(), 400: t.String() },
}
)
.delete(
"/",
async ({ query: { key, fuzzy }, set }) => {
key = validateKey(key);
console.log("DELETE /cache", key);
if (fuzzy) {
key = `*${key}*`;
}
await redis.del(key);
set.status = 204;
return;
},
{
query: t.Object({
...QUERY_TYPE.properties,
...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties,
}),
response: { 204: t.Void(), 400: t.String() },
}
);
function validateKey(key: string) {
const parsedKey = decodeURIComponent(key);
if (parsedKey.length < MIN_LENGTH) {
throw new ModelValidationError(
"Key has to be atleast 1 character long"
);
}
if (parsedKey.includes("*")) {
throw new ModelValidationError("Key cannot contain wildcards");
}
return parsedKey;
}

View File

@@ -0,0 +1,7 @@
import { Elysia } from "elysia";
import { cacheRoutes } from "./cache";
import { apiKeyMiddleware } from "@/middleware/apiKeyMiddleware";
export const apiRoutes = new Elysia({ prefix: "/api" })
.guard({ beforeHandle: apiKeyMiddleware })
.use(cacheRoutes);