feat: ElastiCache
This commit is contained in:
@@ -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" } });
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user