import { ResultAsync, errAsync, okAsync } from "neverthrow"; import { FlowExecCtx } from "@core/flow.execution.context"; import { Database, asc, eq } from "@pkg/db"; import { link } from "@pkg/db/schema"; import { type Err } from "@pkg/result"; import { logger } from "@pkg/logger"; import { traceResultAsync } from "@core/observability"; import { CreateLink, Link, LinkWithDevice, UpdateLink } from "./data"; import { linkErrors } from "./errors"; export class LinkRepository { constructor(private db: Database) {} list(fctx: FlowExecCtx): ResultAsync { return traceResultAsync({ name: "link.list", fctx, fn: () => ResultAsync.fromPromise( this.db.select().from(link).orderBy(asc(link.createdAt)), (e) => linkErrors.listFailed( fctx, e instanceof Error ? e.message : String(e), ), ).map((rows) => rows as Link[]), }); } getById(fctx: FlowExecCtx, id: number): ResultAsync { return traceResultAsync({ name: "link.getById", fctx, fn: () => ResultAsync.fromPromise( this.db.query.link.findFirst({ where: eq(link.id, id) }), (e) => linkErrors.dbError( fctx, e instanceof Error ? e.message : String(e), ), ).andThen((row) => { if (!row) return errAsync(linkErrors.linkNotFound(fctx, id)); return okAsync(row as Link); }), }); } getByToken(fctx: FlowExecCtx, token: string): ResultAsync { return traceResultAsync({ name: "link.getByToken", fctx, fn: () => ResultAsync.fromPromise( this.db.query.link.findFirst({ where: eq(link.token, token), with: { device: true }, }), (e) => linkErrors.dbError( fctx, e instanceof Error ? e.message : String(e), ), ).andThen((row) => { if (!row) return errAsync(linkErrors.linkNotFound(fctx, token)); return okAsync(row as LinkWithDevice); }), }); } create(fctx: FlowExecCtx, data: CreateLink): ResultAsync { logger.info("Creating link", { ...fctx, token: data.token }); return traceResultAsync({ name: "link.create", fctx, fn: () => ResultAsync.fromPromise( this.db .insert(link) .values({ token: data.token, status: "active", appName: data.appName, appPackage: data.appPackage, linkedDeviceId: data.linkedDeviceId ?? null, expiresAt: data.expiresAt ?? null, createdAt: new Date(), updatedAt: new Date(), }) .returning() .execute(), (e) => linkErrors.createFailed( fctx, e instanceof Error ? e.message : String(e), ), ).map((rows) => rows[0] as Link), }); } update( fctx: FlowExecCtx, id: number, updates: UpdateLink, ): ResultAsync { return traceResultAsync({ name: "link.update", fctx, fn: () => this.getById(fctx, id).andThen(() => ResultAsync.fromPromise( this.db .update(link) .set({ ...updates, updatedAt: new Date() }) .where(eq(link.id, id)) .returning() .execute(), (e) => linkErrors.updateFailed( fctx, e instanceof Error ? e.message : String(e), ), ).andThen((rows) => { if (!rows[0]) return errAsync(linkErrors.linkNotFound(fctx, id)); return okAsync(rows[0] as Link); }), ), }); } delete(fctx: FlowExecCtx, id: number): ResultAsync { return traceResultAsync({ name: "link.delete", fctx, fn: () => this.getById(fctx, id).andThen(() => ResultAsync.fromPromise( this.db.delete(link).where(eq(link.id, id)).execute(), (e) => linkErrors.deleteFailed( fctx, e instanceof Error ? e.message : String(e), ), ).map(() => true), ), }); } touch(fctx: FlowExecCtx, token: string): ResultAsync { return traceResultAsync({ name: "link.touch", fctx, fn: () => ResultAsync.fromPromise( this.db .update(link) .set({ lastAccessedAt: new Date(), updatedAt: new Date() }) .where(eq(link.token, token)) .returning() .execute(), (e) => linkErrors.updateFailed( fctx, e instanceof Error ? e.message : String(e), ), ).andThen((rows) => { if (!rows[0]) return errAsync(linkErrors.linkNotFound(fctx, token)); return okAsync(rows[0] as Link); }), }); } }