~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
63 lines · 2.5 KB Raw History
import { defineQuery } from "$hatk";
import type { Gallery } from "$hatk";
import { hydrateGalleries } from "../hydrate/galleries.ts";
import { hideLabelsFilter } from "../labels/_hidden.ts";
import { resolveHandle } from "../helpers/resolveHandle.ts";

export default defineQuery("social.grain.unspecced.getActorFavorites", async (ctx) => {
  const { db, ok, getRecords } = ctx;
  const actor = ctx.params.actor ? await resolveHandle(db, ctx.params.actor) : null;
  if (!actor) return ok({ items: [] });

  // Only the actor themselves can view their favorites. Compared after
  // resolution, so asking by handle works for the owner too.
  if (ctx.viewer?.did !== actor) return ok({ items: [] });

  const limit = Math.min(Number(ctx.params.limit) || 30, 100);
  const cursor = ctx.params.cursor as string | undefined;

  const queryParams: (string | number)[] = [actor, limit + 1];
  let cursorClause = "";
  if (cursor) {
    cursorClause = ` HAVING MAX(f.created_at) < $3`;
    queryParams.push(cursor);
  }

  // GROUP BY f.subject: an account can hold more than one favorite record for
  // the same gallery (verified present in prod — the records are real, they
  // exist in the users' repos), which would otherwise list that gallery twice.
  // MAX(created_at) is the sort key so a re-favorite moves the gallery up, and
  // the cursor compares against the same aggregate.
  const rows = (await db.query(
    `SELECT f.subject AS subject, MAX(f.created_at) AS created_at
     FROM "social.grain.favorite" f
     JOIN "social.grain.gallery" t ON t.uri = f.subject
     LEFT JOIN _repos r ON t.did = r.did
     WHERE f.did = $1
       AND (r.status IS NULL OR r.status NOT IN ('takendown', 'deactivated', 'deleted'))
       AND ${hideLabelsFilter("t.uri")}
       AND (SELECT count(*) FROM "social.grain.gallery.item" gi WHERE gi.gallery = t.uri) > 0
     GROUP BY f.subject
     ${cursorClause}
     ORDER BY created_at DESC
     LIMIT $2`,
    queryParams,
  )) as { subject: string; created_at: string }[];

  const hasMore = rows.length > limit;
  const pageRows = hasMore ? rows.slice(0, limit) : rows;

  const recordsMap = await getRecords<Gallery>(
    "social.grain.gallery",
    pageRows.map((r) => r.subject),
  );

  // Preserve favorite ordering
  const galleryRows = pageRows.map((r) => recordsMap.get(r.subject)).filter((r) => r != null);

  const items = await hydrateGalleries(ctx, galleryRows);

  const nextCursor = hasMore ? pageRows[pageRows.length - 1].created_at : undefined;

  return ok({ items, ...(nextCursor ? { cursor: nextCursor } : {}) });
});