-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreference-server.ts
More file actions
103 lines (90 loc) · 4.27 KB
/
Copy pathreference-server.ts
File metadata and controls
103 lines (90 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// Reference http-sql 0.0.1 server, ~80 lines.
//
// Runs on any platform with `fetch`-style Request/Response (Workers, Deno,
// Bun, Node 20+ with the undici fetch globals). The SQL execution is faked
// here -- replace `execute()` with your actual database call.
interface Statement { sql: string; params?: unknown[]; }
interface SingleRequest { sql: string; params?: unknown[]; }
interface BatchRequest { batch: Statement[]; atomic?: boolean; }
type RequestBody = SingleRequest | BatchRequest;
interface Result {
columns: string[];
rows: unknown[][];
rowsAffected: number;
lastInsertId?: string | number | null;
}
const VERSION = "0.0.1";
// SPEC.md section 9: Http-Sql-Version is the header; X-Http-Sql-Version rides along until 1.0 for older clients.
const VERSION_HEADER = { "Http-Sql-Version": VERSION, "X-Http-Sql-Version": VERSION };
// SPEC.md section 2: responses use the http-sql media type.
const JSON_HEADERS = { "content-type": "application/http-sql+json", ...VERSION_HEADER };
export async function handle(req: Request, auth: (req: Request) => boolean): Promise<Response> {
if (!auth(req)) return errorResponse(401, "auth_error", "missing or invalid bearer token");
if (req.method !== "POST") return errorResponse(405, "bad_request", "POST required");
if (!isJsonMediaType(req.headers.get("content-type"))) {
return errorResponse(415, "unsupported_media_type", "Content-Type must be application/http-sql+json or application/json");
}
let body: RequestBody;
try { body = await req.json(); }
catch { return errorResponse(400, "bad_request", "invalid JSON"); }
const hasSql = "sql" in body && typeof body.sql === "string";
const hasBatch = "batch" in body && Array.isArray(body.batch);
if (hasSql === hasBatch) {
return errorResponse(400, "bad_request", "request must contain exactly one of sql or batch");
}
try {
if (hasSql) {
const result = await execute(body as SingleRequest);
return ok(result);
}
const { batch, atomic = false } = body as BatchRequest;
const results = await executeBatch(batch, atomic);
return ok({ results });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const statementIndex = (err as { statementIndex?: number }).statementIndex;
return errorResponse(400, "sql_error", message, statementIndex);
}
}
// SPEC.md section 2: application/http-sql+json and application/json are interchangeable on
// requests; only the media type is significant, so parameters such as `charset=utf-8` are ignored.
const REQUEST_MEDIA_TYPES = new Set(["application/http-sql+json", "application/json"]);
function isJsonMediaType(header: string | null): boolean {
const mediaType = header?.split(";")[0].trim().toLowerCase();
return mediaType !== undefined && REQUEST_MEDIA_TYPES.has(mediaType);
}
// Replace these with calls to your actual database client.
async function execute(_stmt: Statement): Promise<Result> {
return { columns: [], rows: [], rowsAffected: 0, lastInsertId: null };
}
async function executeBatch(batch: Statement[], atomic: boolean): Promise<Result[]> {
// SPEC.md 6.2.1: batches execute sequentially in array order, stopping at
// the first failure; a non-atomic failure carries error.statementIndex.
// In a real server the atomic branch wraps this loop in a transaction.
const out: Result[] = [];
for (let i = 0; i < batch.length; i++) {
try {
out.push(await execute(batch[i]));
} catch (err) {
if (!atomic && err !== null && typeof err === "object") {
(err as { statementIndex?: number }).statementIndex = i;
}
throw err;
}
}
return out;
}
function ok(body: unknown): Response {
return new Response(JSON.stringify(body), { status: 200, headers: JSON_HEADERS });
}
function errorResponse(status: number, code: string, message: string, statementIndex?: number): Response {
const error: Record<string, unknown> = { code, message };
if (statementIndex !== undefined) error.statementIndex = statementIndex;
return new Response(JSON.stringify({ error }), { status, headers: JSON_HEADERS });
}
// Example boot under Deno / Bun / Workers:
//
// export default { fetch: (req: Request) => handle(req, hasValidBearer) };
//
// where `hasValidBearer` reads the Authorization header and validates the token
// however your app does it.