Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 1x 1x 2x 2x 2x 2x 2x 2x 1x 2x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x | import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { createDal } from "../../dal";
import { getDb } from "../../db";
import { success } from "../../lib/response";
import { logger } from "../../lib/logger";
type HoUser = { id: string; name: string; email: string };
type Variables = { hoUser: HoUser };
const app = new Hono<{ Bindings: CloudflareBindings; Variables: Variables }>();
// Pro entity ids in this codebase are cuid2 (24 chars). Cap defensively to
// avoid an attacker stuffing a megabyte of garbage into a single insert.
const recordViewSchema = z.object({
proId: z.string().min(1).max(128),
});
/**
* POST /api/homeowner/pro-views
*
* Records that the authenticated homeowner viewed a pro page. Powers the
* /account hub's activity bar ("you viewed N pros in {city}"). Fire-and-
* forget at the call site (the marketplace pro page wraps this in
* `ctx.waitUntil` so a slow D1 insert never blocks the page response).
*
* Always returns 202 Accepted — even if the pro doesn't exist or the
* insert fails internally. The caller doesn't care; this is a write-only
* analytics ping.
*/
app.post("/", zValidator("json", recordViewSchema), async (c) => {
const user = c.get("hoUser");
const { proId } = c.req.valid("json");
const db = getDb(c.env.DB);
const dal = createDal(db);
try {
await dal.hoProViews.recordView(user.id, proId);
} catch (err) {
// Never surface to the caller. The pro page rendered fine; we just
// missed an analytics row, which is recoverable on the next view.
logger.error("[ho-pro-views] recordView failed:", err);
}
return success(c, { recorded: true }, 202);
});
/**
* GET /api/homeowner/pro-views/recent
*
* Returns the top city by distinct-pro view count in the trailing N days
* (default 7). Powers the /account hub activity bar's rule 1. Returns
* `null` (not 404) when the user hasn't viewed enough pros to fire the
* rule — the hub treats null as "fall through to rule 2".
*
* Query params:
* days — window length in days. Default 7. Capped at 90.
* minDistinct — minimum distinct pros required. Default 2.
*/
app.get("/recent", async (c) => {
const user = c.get("hoUser");
const daysParam = c.req.query("days");
const minDistinctParam = c.req.query("minDistinct");
const days = Math.min(
Math.max(daysParam ? parseInt(daysParam, 10) : 7, 1),
90,
);
const minDistinct = Math.max(
minDistinctParam ? parseInt(minDistinctParam, 10) : 2,
1,
);
const db = getDb(c.env.DB);
const dal = createDal(db);
const result = await dal.hoProViews.recentByUserCity(user.id, {
days,
minDistinct,
});
return success(c, result);
});
export default app;
|