From e0335c33a9351d03cec67891e762d031ac67516b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 7 Apr 2024 16:24:18 -1000 Subject: [PATCH] Timeline refactors --- server/api/api/v1/accounts/[id]/statuses.ts | 188 ++++++-------------- server/api/api/v1/timelines/home.ts | 96 +++++----- server/api/api/v1/timelines/public.ts | 60 +++---- utils/timelines.ts | 64 +++++++ 4 files changed, 190 insertions(+), 218 deletions(-) create mode 100644 utils/timelines.ts diff --git a/server/api/api/v1/accounts/[id]/statuses.ts b/server/api/api/v1/accounts/[id]/statuses.ts index 705ef428..48e95a3e 100644 --- a/server/api/api/v1/accounts/[id]/statuses.ts +++ b/server/api/api/v1/accounts/[id]/statuses.ts @@ -1,8 +1,13 @@ import { apiRoute, applyConfig } from "@api"; +import type { Prisma, Status, User } from "@prisma/client"; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { + statusToAPI, + type StatusWithRelations, +} from "~database/entities/Status"; import { statusAndUserRelations, userRelations, @@ -56,15 +61,49 @@ export default apiRoute<{ if (!user) return errorResponse("User not found", 404); if (pinned) { - const objects = await client.status.findMany({ - where: { - authorId: id, - isReblog: false, - pinnedBy: { - some: { - id: user.id, + const { objects, link } = await fetchTimeline( + client.status, + { + where: { + authorId: id, + isReblog: false, + pinnedBy: { + some: { + id: user.id, + }, + }, + id: { + lt: max_id, + gt: min_id, + gte: since_id, }, }, + include: statusAndUserRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, + }, + req, + ); + + return jsonResponse( + await Promise.all( + objects.map((status) => statusToAPI(status, user)), + ), + 200, + { + Link: link, + }, + ); + } + + const { objects, link } = await fetchTimeline( + client.status, + { + where: { + authorId: id, + isReblog: exclude_reblogs ? true : undefined, id: { lt: max_id, gt: min_id, @@ -76,142 +115,15 @@ export default apiRoute<{ orderBy: { id: "desc", }, - }); - - // Constuct HTTP Link header (next and prev) only if there are more statuses - const linkHeader = []; - - if (objects.length > 0) { - // Check if there are statuses before the first one - const objectsBefore = await client.status.findMany({ - where: { - authorId: id, - isReblog: false, - pinnedBy: { - some: { - id: user.id, - }, - }, - id: { - gt: objects[0].id, - }, - }, - take: 1, - }); - - if (objectsBefore.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - // Add prev link - linkHeader.push( - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } - - // Check if there are statuses after the last one - const objectsAfter = await client.status.findMany({ - where: { - authorId: id, - isReblog: false, - pinnedBy: { - some: { - id: user.id, - }, - }, - id: { - lt: objects.at(-1)?.id, - }, - }, - take: 1, - }); - - if (objectsAfter.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - // Add next link - linkHeader.push( - `<${urlWithoutQuery}?max_id=${ - objects.at(-1)?.id - }>; rel="next"`, - ); - } - } - - return jsonResponse( - await Promise.all( - objects.map((status) => statusToAPI(status, user)), - ), - 200, - { - Link: linkHeader.join(", "), - }, - ); - } - - const objects = await client.status.findMany({ - where: { - authorId: id, - isReblog: exclude_reblogs ? true : undefined, - id: { - lt: max_id, - gt: min_id, - gte: since_id, - }, }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) only if there are more statuses - const linkHeader = []; - if (objects.length > 0) { - // Check if there are statuses before the first one - const objectsBefore = await client.status.findMany({ - where: { - authorId: id, - isReblog: exclude_reblogs ? true : undefined, - id: { - gt: objects[0].id, - }, - }, - take: 1, - }); - - if (objectsBefore.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - // Add prev link - linkHeader.push( - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } - - // Check if there are statuses after the last one - const objectsAfter = await client.status.findMany({ - where: { - authorId: id, - isReblog: exclude_reblogs ? true : undefined, - id: { - lt: objects.at(-1)?.id, - }, - }, - take: 1, - }); - - if (objectsAfter.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - // Add next link - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, - ); - } - } + req, + ); return jsonResponse( await Promise.all(objects.map((status) => statusToAPI(status, user))), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index f0d62a4c..441f0a61 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -1,7 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { + type StatusWithRelations, + statusToAPI, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -29,63 +33,57 @@ export default apiRoute<{ const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest; - if (limit < 1 || limit > 40) { + if (limit < 1 || limit > 80) { return errorResponse("Limit must be between 1 and 40", 400); } if (!user) return errorResponse("Unauthorized", 401); - const objects = await client.status.findMany({ - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - OR: [ - { - author: { - OR: [ - { - relationshipSubjects: { - some: { - ownerId: user.id, - following: true, + const { objects, link } = await fetchTimeline( + client.status, + { + where: { + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + OR: [ + { + author: { + OR: [ + { + relationshipSubjects: { + some: { + ownerId: user.id, + following: true, + }, }, }, - }, - { - id: user.id, - }, - ], - }, - }, - { - // Include posts where the user is mentioned in addition to posts by followed users - mentions: { - some: { - id: user.id, + { + id: user.id, + }, + ], }, }, - }, - ], + { + // Include posts where the user is mentioned in addition to posts by followed users + mentions: { + some: { + id: user.id, + }, + }, + }, + ], + }, + include: statusAndUserRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( await Promise.all( @@ -93,7 +91,7 @@ export default apiRoute<{ ), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/timelines/public.ts b/server/api/api/v1/timelines/public.ts index 843f2688..81e3642f 100644 --- a/server/api/api/v1/timelines/public.ts +++ b/server/api/api/v1/timelines/public.ts @@ -1,7 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { + statusToAPI, + type StatusWithRelations, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -44,37 +48,31 @@ export default apiRoute<{ return errorResponse("Cannot use both local and remote", 400); } - const objects = await client.status.findMany({ - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, + const { objects, link } = await fetchTimeline( + client.status, + { + where: { + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + instanceId: remote + ? { + not: null, + } + : local + ? null + : undefined, + }, + include: statusAndUserRelations, + take: Number(limit), + orderBy: { + id: "desc", }, - instanceId: remote - ? { - not: null, - } - : local - ? null - : undefined, }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( await Promise.all( @@ -84,7 +82,7 @@ export default apiRoute<{ ), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/utils/timelines.ts b/utils/timelines.ts new file mode 100644 index 00000000..c322dbdc --- /dev/null +++ b/utils/timelines.ts @@ -0,0 +1,64 @@ +import type { Status, User, Prisma } from "@prisma/client"; + +export async function fetchTimeline( + model: Prisma.StatusDelegate | Prisma.UserDelegate, + args: Prisma.StatusFindManyArgs | Prisma.UserFindManyArgs, + req: Request, +) { + // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models + const objects = (await model.findMany(args)) as T[]; + + // Constuct HTTP Link header (next and prev) only if there are more statuses + const linkHeader = []; + + if (objects.length > 0) { + // Check if there are statuses before the first one + // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models + const objectsBefore = await model.findMany({ + where: { + id: { + gt: objects[0].id, + }, + ...args.where, + }, + take: 1, + }); + + if (objectsBefore.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + // Add prev link + linkHeader.push( + `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, + ); + } + + if (objects.length < (args.take ?? Number.POSITIVE_INFINITY)) { + // Check if there are statuses after the last one + // @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models + const objectsAfter = await model.findMany({ + where: { + id: { + lt: objects.at(-1)?.id, + }, + ...args.where, + }, + take: 1, + }); + + if (objectsAfter.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + // Add next link + linkHeader.push( + `<${urlWithoutQuery}?max_id=${ + objects.at(-1)?.id + }>; rel="next"`, + ); + } + } + } + + return { + link: linkHeader.join(", "), + objects, + }; +}