Timeline refactors

This commit is contained in:
Jesse Wierzbinski 2024-04-07 16:24:18 -10:00
parent 69ffd5fafc
commit e0335c33a9
No known key found for this signature in database
4 changed files with 190 additions and 218 deletions

View file

@ -1,8 +1,13 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import type { Prisma, Status, User } from "@prisma/client";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { statusToAPI } from "~database/entities/Status"; import {
statusToAPI,
type StatusWithRelations,
} from "~database/entities/Status";
import { import {
statusAndUserRelations, statusAndUserRelations,
userRelations, userRelations,
@ -56,15 +61,49 @@ export default apiRoute<{
if (!user) return errorResponse("User not found", 404); if (!user) return errorResponse("User not found", 404);
if (pinned) { if (pinned) {
const objects = await client.status.findMany({ const { objects, link } = await fetchTimeline<StatusWithRelations>(
where: { client.status,
authorId: id, {
isReblog: false, where: {
pinnedBy: { authorId: id,
some: { isReblog: false,
id: user.id, 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<StatusWithRelations>(
client.status,
{
where: {
authorId: id,
isReblog: exclude_reblogs ? true : undefined,
id: { id: {
lt: max_id, lt: max_id,
gt: min_id, gt: min_id,
@ -76,142 +115,15 @@ export default apiRoute<{
orderBy: { orderBy: {
id: "desc", 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, req,
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"`,
);
}
}
return jsonResponse( return jsonResponse(
await Promise.all(objects.map((status) => statusToAPI(status, user))), await Promise.all(objects.map((status) => statusToAPI(status, user))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,7 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -29,63 +33,57 @@ export default apiRoute<{
const { limit = 20, max_id, min_id, since_id } = extraData.parsedRequest; 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); return errorResponse("Limit must be between 1 and 40", 400);
} }
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const objects = await client.status.findMany({ const { objects, link } = await fetchTimeline<StatusWithRelations>(
where: { client.status,
id: { {
lt: max_id ?? undefined, where: {
gte: since_id ?? undefined, id: {
gt: min_id ?? undefined, lt: max_id ?? undefined,
}, gte: since_id ?? undefined,
OR: [ gt: min_id ?? undefined,
{ },
author: { OR: [
OR: [ {
{ author: {
relationshipSubjects: { OR: [
some: { {
ownerId: user.id, relationshipSubjects: {
following: true, some: {
ownerId: user.id,
following: true,
},
}, },
}, },
}, {
{ 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 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, req,
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"`,
);
}
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
@ -93,7 +91,7 @@ export default apiRoute<{
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,7 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -44,37 +48,31 @@ export default apiRoute<{
return errorResponse("Cannot use both local and remote", 400); return errorResponse("Cannot use both local and remote", 400);
} }
const objects = await client.status.findMany({ const { objects, link } = await fetchTimeline<StatusWithRelations>(
where: { client.status,
id: { {
lt: max_id ?? undefined, where: {
gte: since_id ?? undefined, id: {
gt: min_id ?? undefined, 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, req,
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"`,
);
}
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
@ -84,7 +82,7 @@ export default apiRoute<{
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

64
utils/timelines.ts Normal file
View file

@ -0,0 +1,64 @@
import type { Status, User, Prisma } from "@prisma/client";
export async function fetchTimeline<T extends User | Status>(
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,
};
}