|
1 | 1 | import { zValidator } from "@hono/zod-validator"; |
| 2 | +import { sql } from "drizzle-orm"; |
2 | 3 | import { Hono } from "hono"; |
| 4 | +import { logger } from "hono/logger"; |
3 | 5 | import { proxy } from "hono/proxy"; |
4 | 6 | import { z } from "zod"; |
5 | 7 |
|
6 | 8 | import { getEnv } from "./env"; |
| 9 | +import { drizzlePersisterMiddleware } from "./middleware/drizzle"; |
7 | 10 | import { supabaseMiddleware } from "./middleware/supabase"; |
8 | 11 | import { renderer } from "./renderer"; |
9 | 12 | import type { Env } from "./types"; |
10 | 13 |
|
11 | 14 | const app = new Hono<Env>(); |
| 15 | +app.use(logger()); |
12 | 16 | app.use("/v1", supabaseMiddleware()); |
13 | 17 |
|
14 | 18 | app.get("/health", (c) => c.text("OK")); |
15 | | -app.get("/", renderer, (c) => { |
| 19 | +app.get("/callback/auth", renderer, (c) => { |
16 | 20 | const params = c.req.query(); |
17 | 21 | const code = params.code; |
18 | 22 | const deeplink = "hypr://auth/callback?" + new URLSearchParams(params).toString(); |
@@ -56,39 +60,87 @@ app.get("/", renderer, (c) => { |
56 | 60 |
|
57 | 61 | app.post( |
58 | 62 | "/v1/write", |
| 63 | + drizzlePersisterMiddleware(), |
59 | 64 | zValidator( |
60 | 65 | "json", |
61 | | - z.discriminatedUnion("operation", [ |
62 | | - z.object({ |
63 | | - table: z.string(), |
64 | | - row_id: z.string(), |
65 | | - operation: z.literal("delete"), |
66 | | - }), |
67 | | - z.object({ |
68 | | - table: z.string(), |
69 | | - row_id: z.string(), |
70 | | - data: z.record(z.string(), z.unknown()), |
71 | | - operation: z.literal("update"), |
72 | | - }), |
73 | | - ]), |
| 66 | + z.array( |
| 67 | + z.discriminatedUnion("operation", [ |
| 68 | + z.object({ |
| 69 | + table: z.string(), |
| 70 | + row_id: z.string(), |
| 71 | + operation: z.literal("delete"), |
| 72 | + }), |
| 73 | + z.object({ |
| 74 | + table: z.string(), |
| 75 | + row_id: z.string(), |
| 76 | + data: z.record(z.string(), z.unknown()), |
| 77 | + operation: z.literal("update"), |
| 78 | + }), |
| 79 | + ]), |
| 80 | + ), |
74 | 81 | ), |
75 | 82 | async (c) => { |
76 | | - const supabase = c.get("supabase"); |
| 83 | + const db = c.get("db"); |
77 | 84 | const user = c.get("user"); |
78 | 85 | const body = c.req.valid("json"); |
79 | 86 |
|
80 | | - // TODO: use RPC / transaction |
81 | | - if (body.operation === "delete") { |
82 | | - await supabase.from(body.table).delete().eq("id", body.row_id); |
83 | | - } else { |
84 | | - await supabase.from(body.table).upsert({ |
85 | | - ...body.data, |
86 | | - id: body.row_id, |
87 | | - user_id: user.id, |
| 87 | + try { |
| 88 | + await db.transaction(async (tx) => { |
| 89 | + for (const change of body) { |
| 90 | + const tableName = sql.identifier(change.table); |
| 91 | + |
| 92 | + if (change.operation === "delete") { |
| 93 | + await tx.execute( |
| 94 | + sql` |
| 95 | + DELETE FROM ${tableName} |
| 96 | + WHERE id = ${change.row_id} |
| 97 | + AND user_id = ${user.id} |
| 98 | + `, |
| 99 | + ); |
| 100 | + } else { |
| 101 | + const protectedFields = new Set(["id", "user_id"]); |
| 102 | + const safeData = Object.fromEntries( |
| 103 | + Object.entries(change.data).filter(([key]) => !protectedFields.has(key)), |
| 104 | + ); |
| 105 | + |
| 106 | + const columns = ["id", "user_id", ...Object.keys(safeData)]; |
| 107 | + const values = [change.row_id, user.id, ...Object.values(safeData)]; |
| 108 | + |
| 109 | + const columnIdentifiers = sql.join( |
| 110 | + columns.map((col) => sql.identifier(col)), |
| 111 | + sql.raw(", "), |
| 112 | + ); |
| 113 | + |
| 114 | + const valuePlaceholders = sql.join( |
| 115 | + values.map((v) => sql`${v}`), |
| 116 | + sql.raw(", "), |
| 117 | + ); |
| 118 | + |
| 119 | + const updateSet = sql.join( |
| 120 | + columns.slice(2).map((col) => { |
| 121 | + const colId = sql.identifier(col); |
| 122 | + return sql`${colId} = EXCLUDED.${colId}`; |
| 123 | + }), |
| 124 | + sql.raw(", "), |
| 125 | + ); |
| 126 | + |
| 127 | + await tx.execute( |
| 128 | + sql` |
| 129 | + INSERT INTO ${tableName} (${columnIdentifiers}) |
| 130 | + VALUES (${valuePlaceholders}) |
| 131 | + ON CONFLICT (id) |
| 132 | + DO UPDATE SET ${updateSet} |
| 133 | + WHERE ${tableName}.user_id = ${user.id} |
| 134 | + `, |
| 135 | + ); |
| 136 | + } |
| 137 | + } |
88 | 138 | }); |
89 | | - } |
90 | 139 |
|
91 | | - return c.json({ message: "OK" }); |
| 140 | + return c.json({ message: "OK" }); |
| 141 | + } catch (error) { |
| 142 | + return c.json({ error }, 500); |
| 143 | + } |
92 | 144 | }, |
93 | 145 | ); |
94 | 146 |
|
|
0 commit comments