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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 18x 2x 2x 2x 1x 1x 1x 67x 5x 5x 16x 16x 14x 14x 6x 6x 3x 5x | import { and, desc, eq, sql } from "drizzle-orm";
import type { DrizzleD1Database } from "drizzle-orm/d1";
import type { RrmLink } from "../../db/schema";
import * as schema from "../../db/schema";
const LINKS = schema.rrmLinks;
/**
* Short links behind `go.interioring.com/l/:slug` (build spec §1.6).
*
* The destination is resolved here and 302'd by the Go worker — never 301, so
* a swapped destination takes effect immediately instead of living in a
* browser cache forever.
*/
export type CreateLinkInput = {
/** The `:slug` in `/l/:slug`. */
slug: string;
/** Absolute URL to 302 to. */
destination: string;
/** Operator-facing name, e.g. "Kokapet WhatsApp group post". */
label?: string | null;
};
export class DuplicateLinkError extends Error {
code = "DUPLICATE_SLUG";
constructor(public slug: string) {
super(`Link slug "${slug}" already exists`);
}
}
export class LinkNotFoundError extends Error {
code = "NOT_FOUND";
constructor(public slug: string) {
super(`Link slug "${slug}" not found`);
}
}
export class RrmLinksDal {
constructor(private db: DrizzleD1Database<typeof schema>) {}
/**
* The destination for a live slug, or `null`.
*
* Unknown and deactivated slugs are the same answer on purpose. §1.6: the
* caller redirects to the marketing home and logs `link_miss` — it never
* shows an error page. Someone who followed a retired link from a WhatsApp
* group six weeks ago is a prospect, and a 404 is a worse first impression
* than the homepage.
*
* `active` is filtered in the WHERE clause rather than checked after the
* fetch so the two states cannot diverge in a caller that forgets.
*/
async resolve(slug: string): Promise<string | null> {
const rows = await this.db
.select({ destination: LINKS.destination })
.from(LINKS)
.where(and(eq(LINKS.slug, slug), eq(LINKS.active, true)))
.limit(1);
return rows[0]?.destination ?? null;
}
/**
* The slug is the primary key, so a duplicate is checked up front rather
* than caught: D1 and the test engine surface constraint violations as
* differently-shaped raw errors, and matching on either one is a bug that
* only appears in the environment you did not test in.
*/
async create(input: CreateLinkInput): Promise<RrmLink> {
const existing = await this.db
.select({ slug: LINKS.slug })
.from(LINKS)
.where(eq(LINKS.slug, input.slug))
.limit(1);
if (existing.length > 0) throw new DuplicateLinkError(input.slug);
const inserted = await this.db
.insert(LINKS)
.values({
slug: input.slug,
destination: input.destination,
label: input.label ?? null,
active: true,
clickCount: 0,
dateCreated: new Date(),
})
.returning();
return inserted[0];
}
/**
* Retire a slug without deleting it. The row stays so its accumulated
* `clickCount` survives, and so a slug is never silently recycled for a
* different destination while old posts still point at it.
*/
async deactivate(slug: string): Promise<void> {
const updated = await this.db
.update(LINKS)
.set({ active: false })
.where(eq(LINKS.slug, slug))
.returning({ slug: LINKS.slug });
if (updated.length === 0) throw new LinkNotFoundError(slug);
}
/** Newest first — the admin screen is mostly "what did I just make". */
async list(): Promise<RrmLink[]> {
return await this.db.select().from(LINKS).orderBy(desc(LINKS.dateCreated));
}
/**
* Count one click.
*
* The increment happens SQL-side, never read-modify-write: clicks arrive
* concurrently and a read-then-write would drop every collision.
*
* The CALLER reports the click under `waitUntil` and never awaits it — the
* 302 has already gone out by then. So this rejecting must not, and cannot,
* fail the redirect. An unknown slug is a silent no-op for the same reason;
* the miss is recorded as a `link_miss` event by the caller, not as a
* throw here.
*/
async recordClick(slug: string): Promise<void> {
await this.db
.update(LINKS)
.set({ clickCount: sql`${LINKS.clickCount} + 1` })
.where(eq(LINKS.slug, slug));
}
}
|