diff --git a/database/entities/Notification.ts b/database/entities/Notification.ts index d513b7f1..d037dd5b 100644 --- a/database/entities/Notification.ts +++ b/database/entities/Notification.ts @@ -1,8 +1,19 @@ import type { APINotification } from "~types/entities/notification"; -import { type StatusWithRelations, statusToAPI } from "./Status"; -import { type UserWithRelations, userToAPI } from "./User"; +import { + type StatusWithRelations, + statusToAPI, + findFirstStatuses, +} from "./Status"; +import { + type UserWithRelations, + userToAPI, + userRelations, + userExtrasTemplate, + transformOutputToUserWithRelations, +} from "./User"; import type { InferSelectModel } from "drizzle-orm"; import type { notification } from "~drizzle/schema"; +import { db } from "~drizzle/db"; export type Notification = InferSelectModel; @@ -11,6 +22,39 @@ export type NotificationWithRelations = Notification & { account: UserWithRelations; }; +export const findManyNotifications = async ( + query: Parameters[0], +): Promise => { + const output = await db.query.notification.findMany({ + ...query, + with: { + ...query?.with, + account: { + with: { + ...userRelations, + }, + extras: userExtrasTemplate("notification_account"), + }, + }, + extras: { + ...query?.extras, + }, + }); + + return await Promise.all( + output.map(async (notif) => ({ + ...notif, + account: transformOutputToUserWithRelations(notif.account), + status: notif.statusId + ? await findFirstStatuses({ + where: (status, { eq }) => + eq(status.id, notif.statusId ?? ""), + }) + : null, + })), + ); +}; + export const notificationToAPI = async ( notification: NotificationWithRelations, ): Promise => { diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 4eb8575c..34059d0f 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -100,6 +100,21 @@ export const statusExtras = { ), }; +export const statusExtrasTemplate = (name: string) => ({ + // @ts-ignore + reblogCount: sql([ + `(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."reblogId" = ${name}.id)`, + ]).as("reblog_count"), + // @ts-ignore + likeCount: sql([ + `(SELECT COUNT(*) FROM "Like" "like" WHERE "like"."likedId" = ${name}.id)`, + ]).as("like_count"), + // @ts-ignore + replyCount: sql([ + `(SELECT COUNT(*) FROM "Status" "status" WHERE "status"."inReplyToPostId" = ${name}.id)`, + ]).as("reply_count"), +}); + /** * Returns whether this status is viewable by a user. * @param user The user to check. @@ -138,6 +153,15 @@ export const findManyStatuses = async ( where: (attachment, { eq }) => eq(attachment.statusId, sql`"status"."id"`), }, + emojis: { + with: { + emoji: { + with: { + instance: true, + }, + }, + }, + }, author: { with: { ...userRelations, diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 668a0747..d146ea14 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -536,7 +536,12 @@ export const userRelations = relations(user, ({ many, one }) => ({ relationshipSubjects: many(relationship, { relationName: "RelationshipToSubject", }), - notifications: many(notification), + notificationsMade: many(notification, { + relationName: "NotificationToAccount", + }), + notificationsReceived: many(notification, { + relationName: "NotificationToNotified", + }), openIdAccounts: many(openIdAccount), flags: many(flag), modNotes: many(modNote), @@ -637,6 +642,24 @@ export const statusRelations = relations(status, ({ many, one }) => ({ reblogs: many(status, { relationName: "StatusToReblog", }), + notifications: many(notification), +})); + +export const notificationRelations = relations(notification, ({ one }) => ({ + account: one(user, { + fields: [notification.accountId], + references: [user.id], + relationName: "NotificationToAccount", + }), + notified: one(user, { + fields: [notification.notifiedId], + references: [user.id], + relationName: "NotificationToNotified", + }), + status: one(status, { + fields: [notification.statusId], + references: [status.id], + }), })); export const likeRelations = relations(like, ({ one }) => ({ diff --git a/server/api/api/v1/follow_requests/index.ts b/server/api/api/v1/follow_requests/index.ts index 30373108..51802c4d 100644 --- a/server/api/api/v1/follow_requests/index.ts +++ b/server/api/api/v1/follow_requests/index.ts @@ -1,9 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; -import { client } from "~database/datasource"; -import { userToAPI, type UserWithRelations } from "~database/entities/User"; -import { userRelations } from "~database/entities/relations"; +import { + findManyUsers, + userToAPI, + type UserWithRelations, +} from "~database/entities/User"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -34,26 +36,19 @@ export default apiRoute<{ if (!user) return errorResponse("Unauthorized", 401); const { objects, link } = await fetchTimeline( - client.user, + findManyUsers, { - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - relationships: { - some: { - subjectId: user.id, - requested: true, - }, - }, - }, - include: userRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, + // @ts-expect-error Yes I KNOW the types are wrong + where: (subject, { lt, gte, gt, and, sql }) => + and( + max_id ? lt(subject.id, max_id) : undefined, + since_id ? gte(subject.id, since_id) : undefined, + min_id ? gt(subject.id, min_id) : undefined, + sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${user.id} AND "Relationship"."ownerId" = ${subject.id} AND "Relationship"."requested" = true)`, + ), + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (subject, { desc }) => desc(subject.id), }, req, ); diff --git a/server/api/api/v1/notifications/index.ts b/server/api/api/v1/notifications/index.ts index b16770fa..13faade7 100644 --- a/server/api/api/v1/notifications/index.ts +++ b/server/api/api/v1/notifications/index.ts @@ -1,13 +1,20 @@ import { apiRoute, applyConfig } from "@api"; -import type { Prisma } from "@prisma/client"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { notificationToAPI } from "~database/entities/Notification"; +import { + findManyNotifications, + notificationToAPI, +} from "~database/entities/Notification"; import { statusAndUserRelations, userRelations, } from "~database/entities/relations"; +import type { + Notification, + NotificationWithRelations, +} from "~database/entities/Notification"; +import { db } from "~drizzle/db"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -52,41 +59,24 @@ export default apiRoute<{ return errorResponse("Can't use both types and exclude_types", 400); } - const { objects, link } = await fetchTimeline< - Prisma.NotificationGetPayload<{ - include: { - account: { - include: typeof userRelations; - }; - status: { - include: typeof statusAndUserRelations; - }; - }; - }> - >( - client.notification, + const { objects, link } = await fetchTimeline( + findManyNotifications, { - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - notifiedId: user.id, - accountId: account_id, - }, - include: { - account: { - include: userRelations, - }, - status: { - include: statusAndUserRelations, - }, - }, - orderBy: { - id: "desc", - }, - take: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + where: (notification, { lt, gte, gt, and, or, eq, inArray, sql }) => + or( + and( + max_id ? lt(notification.id, max_id) : undefined, + since_id ? gte(notification.id, since_id) : undefined, + min_id ? gt(notification.id, min_id) : undefined, + ), + eq(notification.notifiedId, user.id), + eq(notification.accountId, account_id), + ), + with: {}, + limit: Number(limit), + // @ts-expect-error Yes I KNOW the types are wrong + orderBy: (notification, { desc }) => desc(notification.id), }, req, ); diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index a46e47d5..a7fcfb4a 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -65,9 +65,9 @@ export default apiRoute<{ status.authorId, followers.map((f) => f.ownerId), ), */ - // All statuses where the user is mentioned, using table StatusToUser which has a: status.id and b: user.id + // All statuses where the user is mentioned, using table _StatusToUser which has a: status.id and b: user.id // WHERE format (... = ...) - sql`EXISTS (SELECT 1 FROM "StatusToUser" WHERE "StatusToUser"."a" = ${status.id} AND "StatusToUser"."b" = ${user.id})`, + sql`EXISTS (SELECT 1 FROM "_StatusToUser" WHERE "_StatusToUser"."a" = ${status.id} AND "_StatusToUser"."b" = ${user.id})`, // All statuses from users that the user is following // WHERE format (... = ...) sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${status.authorId} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."following" = true)`, diff --git a/tests/utils.ts b/tests/utils.ts index fd022032..53d23be2 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,4 +1,4 @@ -// import { server } from "~index"; +import { server } from "~index"; /** * This allows us to send a test request to the server even when it isnt running @@ -7,9 +7,7 @@ * @returns Response from the server */ export async function sendTestRequest(req: Request) { - console.log(req); - return fetch(req); - // return server.fetch(req); + return server.fetch(req); } export function wrapRelativeUrl(url: string, base_url: string) { diff --git a/utils/timelines.ts b/utils/timelines.ts index f75019d6..12f0e619 100644 --- a/utils/timelines.ts +++ b/utils/timelines.ts @@ -1,13 +1,16 @@ import type { findManyStatuses, Status } from "~database/entities/Status"; import type { findManyUsers, User } from "~database/entities/User"; -import type { Notification } from "~database/entities/Notification"; +import type { + findManyNotifications, + Notification, +} from "~database/entities/Notification"; import type { db } from "~drizzle/db"; export async function fetchTimeline( model: | typeof findManyStatuses | typeof findManyUsers - | typeof db.query.notification.findMany, + | typeof findManyNotifications, args: | Parameters[0] | Parameters[0]