mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Timeline refactors
This commit is contained in:
parent
69ffd5fafc
commit
e0335c33a9
|
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
64
utils/timelines.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue