feat: ElastiCache

This commit is contained in:
Zakaria
2026-10-05 16:03:05 -04:00
parent c0d3e5f402
commit c86b4554c0
27 changed files with 2454 additions and 5 deletions
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test";
import { createElastiCacheRouter } from "./elasticache";
import { type ElastiCacheExplorer } from "../services/elasticacheExplorer";
import { type ElastiCacheManagementService } from "../services/elasticacheManagement";
function service(): ElastiCacheExplorer {
return {
listTargets: async () => [{ resourceId: "orders" }] as never,
ping: async () => ({ ping: "PONG" }) as never,
scan: async () => ({ cursor: "0", keys: ["order:1"] }),
keyDetails: async () => null,
setString: async () => ({ ok: true }),
deleteKey: async () => ({ deleted: true }),
};
}
describe("ElastiCache explorer routes", () => {
test("exposes ElastiCache user management routes", async () => {
const management = {listUsers: async () => [{UserId: "reader"}]} as unknown as ElastiCacheManagementService;
const app = createElastiCacheRouter(service(), management);
const response = await app.request("/users");
expect(response.status).toBe(200);
expect(await response.json()).toEqual([{UserId: "reader"}]);
});
test("proxies safe data explorer requests through the backend service", async () => {
const app = createElastiCacheRouter(service());
const response = await app.request("/keys/scan", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ resourceId: "orders", cursor: "0", count: 10 }),
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ cursor: "0", keys: ["order:1"] });
});
test("returns a clean validation error for malformed JSON", async () => {
const app = createElastiCacheRouter(service());
const response = await app.request("/connect", { method: "POST", body: "not json" });
expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: { code: "VALIDATION_ERROR", message: "request body must be a JSON object" } });
});
});
+63
View File
@@ -0,0 +1,63 @@
import { Hono } from "hono";
import {
elasticacheExplorer,
ElastiCacheExplorerError,
type ElastiCacheExplorer,
} from "../services/elasticacheExplorer";
import {
elasticacheManagementService,
type CacheParameterGroupInput,
type CacheParameterInput,
type CacheSubnetGroupInput,
type ElastiCacheManagementService,
type ElastiCacheUserInput,
} from "../services/elasticacheManagement";
export function createElastiCacheRouter(service: ElastiCacheExplorer = elasticacheExplorer, management: ElastiCacheManagementService = elasticacheManagementService) {
const app = new Hono();
app.get("/targets", async (c) => respond(c, () => service.listTargets()));
app.post("/connect", async (c) => respond(c, async () => service.ping(await requestBody(c) as Parameters<ElastiCacheExplorer["ping"]>[0])));
app.post("/keys/scan", async (c) => respond(c, async () => service.scan(await requestBody(c) as Parameters<ElastiCacheExplorer["scan"]>[0])));
app.post("/keys/details", async (c) => respond(c, async () => service.keyDetails(await requestBody(c) as Parameters<ElastiCacheExplorer["keyDetails"]>[0])));
app.post("/keys/set", async (c) => respond(c, async () => service.setString(await requestBody(c) as Parameters<ElastiCacheExplorer["setString"]>[0])));
app.post("/keys/delete", async (c) => respond(c, async () => service.deleteKey(await requestBody(c) as Parameters<ElastiCacheExplorer["deleteKey"]>[0])));
app.get("/users", async (c) => c.json(await management.listUsers()));
app.post("/users", async (c) => c.json(await management.createUser(await c.req.json<ElastiCacheUserInput>()), 201));
app.patch("/users/:userId", async (c) => c.json(await management.modifyUser(c.req.param("userId"), await c.req.json<Omit<ElastiCacheUserInput, "userId" | "userName" | "engine">>())));
app.delete("/users/:userId", async (c) => c.json(await management.deleteUser(c.req.param("userId"))));
app.get("/subnet-groups", async (c) => c.json(await management.listSubnetGroups()));
app.post("/subnet-groups", async (c) => c.json(await management.createSubnetGroup(await c.req.json<CacheSubnetGroupInput>()), 201));
app.patch("/subnet-groups/:name", async (c) => c.json(await management.modifySubnetGroup(c.req.param("name"), await c.req.json<Omit<CacheSubnetGroupInput, "cacheSubnetGroupName">>())));
app.delete("/subnet-groups/:name", async (c) => c.json(await management.deleteSubnetGroup(c.req.param("name"))));
app.get("/parameter-groups", async (c) => c.json(await management.listParameterGroups()));
app.post("/parameter-groups", async (c) => c.json(await management.createParameterGroup(await c.req.json<CacheParameterGroupInput>()), 201));
app.delete("/parameter-groups/:name", async (c) => c.json(await management.deleteParameterGroup(c.req.param("name"))));
app.get("/parameter-groups/:name/parameters", async (c) => c.json(await management.listParameters(c.req.param("name"))));
app.put("/parameter-groups/:name/parameters", async (c) => c.json(await management.setParameters(c.req.param("name"), (await c.req.json<{ parameters: CacheParameterInput[] }>()).parameters)));
return app;
}
export default createElastiCacheRouter();
async function requestBody(c: { req: { json(): Promise<unknown> } }): Promise<Record<string, unknown>> {
try {
const body = await c.req.json();
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error();
return body as Record<string, unknown>;
} catch {
throw new ElastiCacheExplorerError("request body must be a JSON object");
}
}
async function respond(c: { json(data: unknown, status?: number): Response }, operation: () => Promise<unknown>): Promise<Response> {
try {
return c.json(await operation());
} catch (error) {
if (error instanceof ElastiCacheExplorerError) {
return c.json({ error: { code: error.code, message: error.message } }, error.status);
}
return c.json({ error: { code: "INTERNAL_ERROR", message: "ElastiCache explorer request failed" } }, 500);
}
}