mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
perf(api): ⚡ Store user and post metrics directly in database instead of recalculating them on-the-fly
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 0s
Build Docker Images / lint (push) Failing after 6s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 1s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 1s
Nix Build / check (push) Failing after 1s
This commit is contained in:
parent
cd12ccd6c1
commit
ddb3cfc978
|
|
@ -1,13 +1,12 @@
|
|||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||
|
||||
const { users, deleteUsers } = await getTestUsers(2);
|
||||
const { users, deleteUsers } = await getTestUsers(3);
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteUsers();
|
||||
});
|
||||
|
||||
// /api/v1/accounts/:id/follow
|
||||
describe("/api/v1/accounts/:id/follow", () => {
|
||||
test("should return 401 if not authenticated", async () => {
|
||||
await using client = await generateClient();
|
||||
|
|
@ -35,6 +34,16 @@ describe("/api/v1/accounts/:id/follow", () => {
|
|||
const { ok } = await client.followAccount(users[1].id);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
|
||||
const { ok: ok2, data: data2 } = await client.getAccount(users[1].id);
|
||||
|
||||
expect(ok2).toBe(true);
|
||||
expect(data2.followers_count).toBe(1);
|
||||
|
||||
const { ok: ok3, data: data3 } = await client.getAccount(users[0].id);
|
||||
|
||||
expect(ok3).toBe(true);
|
||||
expect(data3.following_count).toBe(1);
|
||||
});
|
||||
|
||||
test("should return 200 if user already followed", async () => {
|
||||
|
|
|
|||
60
api/api/v1/accounts/[id]/unfollow.test.ts
Normal file
60
api/api/v1/accounts/[id]/unfollow.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||
|
||||
const { users, deleteUsers } = await getTestUsers(3);
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteUsers();
|
||||
});
|
||||
|
||||
describe("/api/v1/accounts/:id/unfollow", () => {
|
||||
test("should return 401 if not authenticated", async () => {
|
||||
await using client = await generateClient();
|
||||
|
||||
const { ok, raw } = await client.unfollowAccount(users[1].id);
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(raw.status).toBe(401);
|
||||
});
|
||||
|
||||
test("should return 404 if user not found", async () => {
|
||||
await using client = await generateClient(users[0]);
|
||||
|
||||
const { ok, raw } = await client.unfollowAccount(
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
);
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(raw.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should unfollow user", async () => {
|
||||
await using client = await generateClient(users[0]);
|
||||
|
||||
const { ok } = await client.followAccount(users[1].id);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
|
||||
const { ok: ok2 } = await client.unfollowAccount(users[1].id);
|
||||
|
||||
expect(ok2).toBe(true);
|
||||
|
||||
const { ok: ok3, data } = await client.getAccount(users[1].id);
|
||||
|
||||
expect(ok3).toBe(true);
|
||||
expect(data.followers_count).toBe(0);
|
||||
|
||||
const { ok: ok4, data: data4 } = await client.getAccount(users[0].id);
|
||||
|
||||
expect(ok4).toBe(true);
|
||||
expect(data4.following_count).toBe(0);
|
||||
});
|
||||
|
||||
test("should return 200 if user already followed", async () => {
|
||||
await using client = await generateClient(users[0]);
|
||||
|
||||
const { ok } = await client.followAccount(users[1].id);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -46,4 +46,29 @@ describe("POST /api/v1/statuses/:id/reblog", () => {
|
|||
expect(ok).toBe(true);
|
||||
expect(data.reblog?.id).toBe(statuses[0].id);
|
||||
});
|
||||
|
||||
test("should update reblog count on original status", async () => {
|
||||
await using client = await generateClient(users[0]);
|
||||
|
||||
// Unreblog the status first
|
||||
await client.unreblogStatus(statuses[0].id);
|
||||
|
||||
// Check that the reblog count is 0
|
||||
const { ok: ok1, data: data1 } = await client.getStatus(statuses[0].id);
|
||||
|
||||
expect(ok1).toBe(true);
|
||||
expect(data1.reblogs_count).toBe(0);
|
||||
|
||||
const { ok: ok2, data: data2 } = await client.reblogStatus(
|
||||
statuses[0].id,
|
||||
);
|
||||
|
||||
expect(ok2).toBe(true);
|
||||
expect(data2.reblog?.reblogs_count).toBe(1);
|
||||
|
||||
const { ok: ok3, data: data3 } = await client.getStatus(statuses[0].id);
|
||||
|
||||
expect(ok3).toBe(true);
|
||||
expect(data3.reblogs_count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,4 +48,28 @@ describe("POST /api/v1/statuses/:id/unreblog", () => {
|
|||
expect(ok).toBe(true);
|
||||
expect(data.reblog).toBeNull();
|
||||
});
|
||||
|
||||
test("should update reblog count on original status", async () => {
|
||||
await using client = await generateClient(users[0]);
|
||||
|
||||
// Reblog the status first
|
||||
await client.reblogStatus(statuses[0].id);
|
||||
|
||||
// Check that the reblog count is 1
|
||||
const { ok: ok1, data: data1 } = await client.getStatus(statuses[0].id);
|
||||
|
||||
expect(ok1).toBe(true);
|
||||
expect(data1.reblogs_count).toBe(1);
|
||||
|
||||
const { ok: ok2, data: data2 } = await client.unreblogStatus(
|
||||
statuses[0].id,
|
||||
);
|
||||
|
||||
expect(ok2).toBe(true);
|
||||
expect(data2.reblogs_count).toBe(0);
|
||||
|
||||
const { ok: ok3, data: data3 } = await client.getStatus(statuses[0].id);
|
||||
expect(ok3).toBe(true);
|
||||
expect(data3.reblogs_count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,12 +106,8 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
|
|||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Likes).where(inArray(Likes.id, ids));
|
||||
} else {
|
||||
await db.delete(Likes).where(eq(Likes.id, this.id));
|
||||
}
|
||||
public async delete(): Promise<void> {
|
||||
await db.delete(Likes).where(eq(Likes.id, this.id));
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
|
|
@ -119,13 +115,13 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
|
|||
): Promise<Like> {
|
||||
const inserted = (await db.insert(Likes).values(data).returning())[0];
|
||||
|
||||
const role = await Like.fromId(inserted.id);
|
||||
const like = await Like.fromId(inserted.id);
|
||||
|
||||
if (!role) {
|
||||
if (!like) {
|
||||
throw new Error("Failed to insert like");
|
||||
}
|
||||
|
||||
return role;
|
||||
return like;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Status } from "@versia/client/schemas";
|
|||
import { db, Instance } from "@versia/kit/db";
|
||||
import {
|
||||
EmojiToNote,
|
||||
Likes,
|
||||
MediasToNotes,
|
||||
Notes,
|
||||
NoteToMentions,
|
||||
|
|
@ -49,9 +50,6 @@ type NoteTypeWithRelations = NoteType & {
|
|||
reply: NoteType | null;
|
||||
quote: NoteType | null;
|
||||
application: typeof Application.$type | null;
|
||||
reblogCount: number;
|
||||
likeCount: number;
|
||||
replyCount: number;
|
||||
pinned: boolean;
|
||||
reblogged: boolean;
|
||||
muted: boolean;
|
||||
|
|
@ -104,6 +102,16 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
throw new Error("Failed to insert status");
|
||||
}
|
||||
|
||||
// Update author's status count
|
||||
await note.author.recalculateStatusCount();
|
||||
|
||||
if (note.data.replyId) {
|
||||
// Update the reply's reply count
|
||||
await new Note(
|
||||
note.data.reply as typeof Note.$type,
|
||||
).recalculateReplyCount();
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +316,24 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
return this.author.local;
|
||||
}
|
||||
|
||||
public async recalculateReblogCount(): Promise<void> {
|
||||
const reblogCount = await db.$count(Notes, eq(Notes.reblogId, this.id));
|
||||
|
||||
await this.update({ reblogCount });
|
||||
}
|
||||
|
||||
public async recalculateLikeCount(): Promise<void> {
|
||||
const likeCount = await db.$count(Likes, eq(Likes.likedId, this.id));
|
||||
|
||||
await this.update({ likeCount });
|
||||
}
|
||||
|
||||
public async recalculateReplyCount(): Promise<void> {
|
||||
const replyCount = await db.$count(Notes, eq(Notes.replyId, this.id));
|
||||
|
||||
await this.update({ replyCount });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the emojis associated with this note in the database
|
||||
*
|
||||
|
|
@ -531,12 +557,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
return note;
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Notes).where(inArray(Notes.id, ids));
|
||||
} else {
|
||||
await db.delete(Notes).where(eq(Notes.id, this.id));
|
||||
}
|
||||
public async delete(): Promise<void> {
|
||||
await db.delete(Notes).where(eq(Notes.id, this.id));
|
||||
|
||||
// Update author's status count
|
||||
await this.author.recalculateStatusCount();
|
||||
}
|
||||
|
||||
public async update(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
import type { z } from "zod";
|
||||
import {
|
||||
transformOutputToUserWithRelations,
|
||||
userExtrasTemplate,
|
||||
userRelations,
|
||||
} from "../functions/user.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
|
@ -78,7 +77,6 @@ export class Notification extends BaseInterface<
|
|||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notifications_account"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -112,7 +110,6 @@ export class Notification extends BaseInterface<
|
|||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notifications_account"),
|
||||
},
|
||||
},
|
||||
extras: extra?.extras,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { Relationships } from "@versia/kit/tables";
|
||||
import { Relationships, Users } from "@versia/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
type InferSelectModel,
|
||||
inArray,
|
||||
type SQL,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
|
@ -246,6 +247,40 @@ export class Relationship extends BaseInterface<
|
|||
.set(newRelationship)
|
||||
.where(eq(Relationships.id, this.id));
|
||||
|
||||
// If a user follows another user, update followerCount and followingCount
|
||||
if (newRelationship.following && !this.data.following) {
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followingCount: sql`${Users.followingCount} + 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.ownerId));
|
||||
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followerCount: sql`${Users.followerCount} + 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.subjectId));
|
||||
}
|
||||
|
||||
// If a user unfollows another user, update followerCount and followingCount
|
||||
if (!newRelationship.following && this.data.following) {
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followingCount: sql`${Users.followingCount} - 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.ownerId));
|
||||
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followerCount: sql`${Users.followerCount} - 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.subjectId));
|
||||
}
|
||||
|
||||
const updated = await Relationship.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Notes,
|
||||
NoteToMentions,
|
||||
Notifications,
|
||||
Relationships,
|
||||
Users,
|
||||
UserToPinnedNotes,
|
||||
} from "@versia/kit/tables";
|
||||
|
|
@ -203,6 +204,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
languages: options?.languages,
|
||||
});
|
||||
|
||||
if (!otherUser.data.isLocked) {
|
||||
// Update the follower count
|
||||
await otherUser.recalculateFollowerCount();
|
||||
await this.recalculateFollowingCount();
|
||||
}
|
||||
|
||||
if (otherUser.remote) {
|
||||
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
||||
entity: {
|
||||
|
|
@ -237,6 +244,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
});
|
||||
}
|
||||
|
||||
await this.recalculateFollowingCount();
|
||||
await followee.recalculateFollowerCount();
|
||||
|
||||
await relationship.update({
|
||||
following: false,
|
||||
});
|
||||
|
|
@ -262,6 +272,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
throw new Error("Followee must be a local user");
|
||||
}
|
||||
|
||||
await follower.recalculateFollowerCount();
|
||||
await this.recalculateFollowingCount();
|
||||
|
||||
const entity = new VersiaEntities.FollowAccept({
|
||||
type: "FollowAccept",
|
||||
id: crypto.randomUUID(),
|
||||
|
|
@ -504,6 +517,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
uri: uri?.href,
|
||||
});
|
||||
|
||||
await note.recalculateReblogCount();
|
||||
|
||||
// Refetch the note *again* to get the proper value of .reblogged
|
||||
const finalNewReblog = await Note.fromId(newReblog.id, this?.id);
|
||||
|
||||
|
|
@ -555,6 +570,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
|
||||
await reblogToDelete.delete();
|
||||
|
||||
await note.recalculateReblogCount();
|
||||
|
||||
if (note.author.local) {
|
||||
// Remove any eventual notifications for this reblog
|
||||
await db
|
||||
|
|
@ -586,6 +603,45 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
}
|
||||
}
|
||||
|
||||
public async recalculateFollowerCount(): Promise<void> {
|
||||
const followerCount = await db.$count(
|
||||
Relationships,
|
||||
and(
|
||||
eq(Relationships.subjectId, this.id),
|
||||
eq(Relationships.following, true),
|
||||
),
|
||||
);
|
||||
|
||||
await this.update({
|
||||
followerCount,
|
||||
});
|
||||
}
|
||||
|
||||
public async recalculateFollowingCount(): Promise<void> {
|
||||
const followingCount = await db.$count(
|
||||
Relationships,
|
||||
and(
|
||||
eq(Relationships.ownerId, this.id),
|
||||
eq(Relationships.following, true),
|
||||
),
|
||||
);
|
||||
|
||||
await this.update({
|
||||
followingCount,
|
||||
});
|
||||
}
|
||||
|
||||
public async recalculateStatusCount(): Promise<void> {
|
||||
const statusCount = await db.$count(
|
||||
Notes,
|
||||
and(eq(Notes.authorId, this.id)),
|
||||
);
|
||||
|
||||
await this.update({
|
||||
statusCount,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Like a note.
|
||||
*
|
||||
|
|
@ -611,6 +667,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
uri: uri?.href,
|
||||
});
|
||||
|
||||
await note.recalculateLikeCount();
|
||||
|
||||
if (note.author.local) {
|
||||
// Notify the user that their post has been favourited
|
||||
await note.author.notify("favourite", this, note);
|
||||
|
|
@ -650,6 +708,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
|
||||
await likeToDelete.delete();
|
||||
|
||||
await note.recalculateLikeCount();
|
||||
|
||||
if (note.author.local) {
|
||||
// Remove any eventual notifications for this like
|
||||
await likeToDelete.clearRelatedNotifications();
|
||||
|
|
|
|||
|
|
@ -20,11 +20,7 @@ import { mentionValidator } from "@/api";
|
|||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||
import { config } from "~/config.ts";
|
||||
import type * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
||||
import {
|
||||
transformOutputToUserWithRelations,
|
||||
userExtrasTemplate,
|
||||
userRelations,
|
||||
} from "./user.ts";
|
||||
import { transformOutputToUserWithRelations, userRelations } from "./user.ts";
|
||||
|
||||
/**
|
||||
* Wrapper against the Status object to make it easier to work with
|
||||
|
|
@ -58,7 +54,6 @@ export const findManyNotes = async (
|
|||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notes_author"),
|
||||
},
|
||||
mentions: {
|
||||
with: {
|
||||
|
|
@ -92,9 +87,6 @@ export const findManyNotes = async (
|
|||
with: {
|
||||
user: {
|
||||
with: userRelations,
|
||||
extras: userExtrasTemplate(
|
||||
"Notes_reblog_mentions_user",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -102,22 +94,9 @@ export const findManyNotes = async (
|
|||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
extras: userExtrasTemplate("Notes_reblog_author"),
|
||||
},
|
||||
},
|
||||
extras: {
|
||||
reblogCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes_reblog".id)`.as(
|
||||
"reblog_count",
|
||||
),
|
||||
likeCount:
|
||||
sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id)`.as(
|
||||
"like_count",
|
||||
),
|
||||
replyCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes_reblog".id)`.as(
|
||||
"reply_count",
|
||||
),
|
||||
pinned: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||
"pinned",
|
||||
|
|
@ -144,18 +123,6 @@ export const findManyNotes = async (
|
|||
quote: true,
|
||||
},
|
||||
extras: {
|
||||
reblogCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes".id)`.as(
|
||||
"reblog_count",
|
||||
),
|
||||
likeCount:
|
||||
sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes".id)`.as(
|
||||
"like_count",
|
||||
),
|
||||
replyCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes".id)`.as(
|
||||
"reply_count",
|
||||
),
|
||||
pinned: userId
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||
"pinned",
|
||||
|
|
@ -200,17 +167,11 @@ export const findManyNotes = async (
|
|||
(attachment) => attachment.media,
|
||||
),
|
||||
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
||||
reblogCount: Number(post.reblog.reblogCount),
|
||||
likeCount: Number(post.reblog.likeCount),
|
||||
replyCount: Number(post.reblog.replyCount),
|
||||
pinned: Boolean(post.reblog.pinned),
|
||||
reblogged: Boolean(post.reblog.reblogged),
|
||||
muted: Boolean(post.reblog.muted),
|
||||
liked: Boolean(post.reblog.liked),
|
||||
},
|
||||
reblogCount: Number(post.reblogCount),
|
||||
likeCount: Number(post.likeCount),
|
||||
replyCount: Number(post.replyCount),
|
||||
pinned: Boolean(post.pinned),
|
||||
reblogged: Boolean(post.reblogged),
|
||||
muted: Boolean(post.muted),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
type User,
|
||||
} from "@versia/kit/db";
|
||||
import type { Users } from "@versia/kit/tables";
|
||||
import { type InferSelectModel, type SQL, sql } from "drizzle-orm";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
|
||||
export const userRelations = {
|
||||
instance: true,
|
||||
|
|
@ -32,42 +32,6 @@ export const userRelations = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
export const userExtras = {
|
||||
followerCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = "Users".id AND "relationships"."following" = true))`.as(
|
||||
"follower_count",
|
||||
),
|
||||
followingCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "Users".id AND "relationshipSubjects"."following" = true))`.as(
|
||||
"following_count",
|
||||
),
|
||||
statusCount:
|
||||
sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = "Users".id)`.as(
|
||||
"status_count",
|
||||
),
|
||||
};
|
||||
|
||||
export const userExtrasTemplate = (
|
||||
name: string,
|
||||
): {
|
||||
followerCount: SQL.Aliased<unknown>;
|
||||
followingCount: SQL.Aliased<unknown>;
|
||||
statusCount: SQL.Aliased<unknown>;
|
||||
} => ({
|
||||
// @ts-expect-error sql is a template tag, so it gets confused when we use it as a function
|
||||
followerCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationships" "relationships" WHERE ("relationships"."ownerId" = "${name}".id AND "relationships"."following" = true))`,
|
||||
]).as("follower_count"),
|
||||
// @ts-expect-error sql is a template tag, so it gets confused when we use it as a function
|
||||
followingCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationships" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "${name}".id AND "relationshipSubjects"."following" = true))`,
|
||||
]).as("following_count"),
|
||||
// @ts-expect-error sql is a template tag, so it gets confused when we use it as a function
|
||||
statusCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."authorId" = "${name}".id)`,
|
||||
]).as("status_count"),
|
||||
});
|
||||
|
||||
export interface AuthData {
|
||||
user: User | null;
|
||||
token: Token | null;
|
||||
|
|
@ -131,10 +95,6 @@ export const findManyUsers = async (
|
|||
...userRelations,
|
||||
...query?.with,
|
||||
},
|
||||
extras: {
|
||||
...userExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
||||
|
|
|
|||
|
|
@ -417,7 +417,22 @@ export class InboxProcessor {
|
|||
);
|
||||
}
|
||||
|
||||
await like.delete();
|
||||
const likeAuthor = await User.fromId(like.data.likerId);
|
||||
const liked = await Note.fromId(like.data.likedId);
|
||||
|
||||
if (!liked) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Liked Note not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
if (!likeAuthor) {
|
||||
throw new ApiError(404, "Like author not found");
|
||||
}
|
||||
|
||||
await likeAuthor.unlike(liked);
|
||||
|
||||
return;
|
||||
}
|
||||
case "pub.versia:shares/Share": {
|
||||
|
|
|
|||
6
drizzle/migrations/0050_thick_lester.sql
Normal file
6
drizzle/migrations/0050_thick_lester.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE "Notes" ADD COLUMN "reblog_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ADD COLUMN "like_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ADD COLUMN "reply_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "follower_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "following_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "status_count" integer DEFAULT 0 NOT NULL;
|
||||
2384
drizzle/migrations/meta/0050_snapshot.json
Normal file
2384
drizzle/migrations/meta/0050_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -351,6 +351,13 @@
|
|||
"when": 1744979203068,
|
||||
"tag": "0049_graceful_iron_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 50,
|
||||
"version": "7",
|
||||
"when": 1746368175263,
|
||||
"tag": "0050_thick_lester",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,6 +446,9 @@ export const Notes = pgTable("Notes", {
|
|||
visibility: text("visibility")
|
||||
.$type<z.infer<typeof StatusSchema.shape.visibility>>()
|
||||
.notNull(),
|
||||
reblogCount: integer("reblog_count").default(0).notNull(),
|
||||
likeCount: integer("like_count").default(0).notNull(),
|
||||
replyCount: integer("reply_count").default(0).notNull(),
|
||||
replyId: uuid("replyId").references((): AnyPgColumn => Notes.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
|
|
@ -583,6 +586,9 @@ export const Users = pgTable(
|
|||
onDelete: "set null",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
followerCount: integer("follower_count").default(0).notNull(),
|
||||
followingCount: integer("following_count").default(0).notNull(),
|
||||
statusCount: integer("status_count").default(0).notNull(),
|
||||
createdAt: createdAt(),
|
||||
updatedAt: updatedAt(),
|
||||
isBot: boolean("is_bot").default(false).notNull(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue