Skip to content

Commit b8383ca

Browse files
committed
Implement KV storage endpoints for Local Explorer API
Add REST API endpoints for KV storage operations: - GET /storage/kv/namespaces - List namespaces - GET /storage/kv/namespaces/:id/keys - List keys - GET /storage/kv/namespaces/:id/values/:key - Get value - PUT /storage/kv/namespaces/:id/values/:key - Write value - DELETE /storage/kv/namespaces/:id/values/:key - Delete key - POST /storage/kv/namespaces/:id/bulk/get - Bulk get values
1 parent 2ebaef9 commit b8383ca

File tree

5 files changed

+1044
-6
lines changed

5 files changed

+1044
-6
lines changed

fixtures/worker-with-resources/wrangler.jsonc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
{
88
"binding": "KV",
99
},
10+
{
11+
"binding": "KV_WITH_ID",
12+
"id": "some-kv-id",
13+
},
1014
],
1115
"d1_databases": [
1216
{
Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
11
// local explorer API Worker
22
// Provides a REST API for viewing and manipulating user resources
33

4-
import { WorkerEntrypoint } from "cloudflare:workers";
4+
import { fromHono } from "chanfana";
5+
import { Hono } from "hono";
6+
import {
7+
BulkGetKVValues,
8+
DeleteKVValue,
9+
GetKVValue,
10+
ListKVKeys,
11+
ListKVNamespaces,
12+
PutKVValue,
13+
} from "./resources/kv";
14+
import type { AppBindings } from "./common";
515

6-
export default class LocalExplorerAPI extends WorkerEntrypoint {
7-
async fetch(): Promise<Response> {
8-
return new Response("Hello from local explorer API");
9-
}
10-
}
16+
const BASE_PATH = "/cdn-cgi/explorer/api";
17+
18+
const app = new Hono<AppBindings>().basePath(BASE_PATH);
19+
20+
const openapi = fromHono(app, {
21+
docs_url: "/docs",
22+
schema: {
23+
openapi: "3.0.0",
24+
info: {
25+
title: "Local Explorer API",
26+
version: "1.0.0",
27+
description:
28+
"A local subset of Cloudflare's REST API for exploring resources during local development.",
29+
},
30+
},
31+
});
32+
33+
// ============================================================================
34+
// KV Endpoints
35+
// ============================================================================
36+
37+
openapi.get("/storage/kv/namespaces", ListKVNamespaces);
38+
openapi.get("/storage/kv/namespaces/:namespaceId/keys", ListKVKeys);
39+
openapi.get("/storage/kv/namespaces/:namespaceId/values/:keyName", GetKVValue);
40+
openapi.put("/storage/kv/namespaces/:namespaceId/values/:keyName", PutKVValue);
41+
openapi.delete(
42+
"/storage/kv/namespaces/:namespaceId/values/:keyName",
43+
DeleteKVValue
44+
);
45+
openapi.post("/storage/kv/namespaces/:namespaceId/bulk/get", BulkGetKVValues);
46+
47+
export default app;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { z } from "zod";
2+
3+
// ============================================================================
4+
// Zod Schemas (for OpenAPI docs and runtime validation)
5+
// ============================================================================
6+
//
7+
// NOTE: Do not use z.coerce for query/path parameters. The chanfana library
8+
// automatically handles string-to-number coercion for these parameters.
9+
// Using z.coerce is redundant and unnecessary.
10+
11+
export const ResponseInfoSchema = z.object({
12+
code: z.number(),
13+
message: z.string(),
14+
});
15+
16+
// Page-based pagination schema (for namespace listing)
17+
export const PageResultInfoSchema = z.object({
18+
count: z.number(),
19+
page: z.number(),
20+
per_page: z.number(),
21+
total_count: z.number(),
22+
});
23+
24+
// Cursor-based pagination schema (for key listing)
25+
export const CursorResultInfoSchema = z.object({
26+
count: z.number(),
27+
cursor: z.string(),
28+
});
29+
30+
// Helper to create envelope schema for OpenAPI docs
31+
export const CloudflareEnvelope = <T extends z.ZodTypeAny>(resultSchema: T) =>
32+
z.object({
33+
success: z.boolean(),
34+
errors: z.array(ResponseInfoSchema),
35+
messages: z.array(ResponseInfoSchema),
36+
result: resultSchema,
37+
});
38+
39+
// ============================================================================
40+
// Response Helpers
41+
// ============================================================================
42+
43+
// Wrap result in Cloudflare envelope
44+
export function wrapResponse<T>(result: T) {
45+
return {
46+
success: true as const,
47+
errors: [] as { code: number; message: string }[],
48+
messages: [] as { code: number; message: string }[],
49+
result,
50+
};
51+
}
52+
53+
// Create error response
54+
export function errorResponse(status: number, code: number, message: string) {
55+
return Response.json(
56+
{
57+
success: false,
58+
errors: [{ code, message }],
59+
messages: [],
60+
result: null,
61+
},
62+
{ status }
63+
);
64+
}
65+
66+
// ============================================================================
67+
// Shared Types
68+
// ============================================================================
69+
70+
export interface Env {
71+
[key: string]: unknown;
72+
}
73+
74+
export type AppBindings = { Bindings: Env };

0 commit comments

Comments
 (0)