mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38: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 { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await deleteUsers();
|
await deleteUsers();
|
||||||
});
|
});
|
||||||
|
|
||||||
// /api/v1/accounts/:id/follow
|
|
||||||
describe("/api/v1/accounts/:id/follow", () => {
|
describe("/api/v1/accounts/:id/follow", () => {
|
||||||
test("should return 401 if not authenticated", async () => {
|
test("should return 401 if not authenticated", async () => {
|
||||||
await using client = await generateClient();
|
await using client = await generateClient();
|
||||||
|
|
@ -35,6 +34,16 @@ describe("/api/v1/accounts/:id/follow", () => {
|
||||||
const { ok } = await client.followAccount(users[1].id);
|
const { ok } = await client.followAccount(users[1].id);
|
||||||
|
|
||||||
expect(ok).toBe(true);
|
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 () => {
|
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(ok).toBe(true);
|
||||||
expect(data.reblog?.id).toBe(statuses[0].id);
|
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(ok).toBe(true);
|
||||||
expect(data.reblog).toBeNull();
|
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,26 +106,22 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
|
||||||
return this.update(this.data);
|
return this.update(this.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(ids?: string[]): Promise<void> {
|
public async delete(): 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));
|
await db.delete(Likes).where(eq(Likes.id, this.id));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static async insert(
|
public static async insert(
|
||||||
data: InferInsertModel<typeof Likes>,
|
data: InferInsertModel<typeof Likes>,
|
||||||
): Promise<Like> {
|
): Promise<Like> {
|
||||||
const inserted = (await db.insert(Likes).values(data).returning())[0];
|
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");
|
throw new Error("Failed to insert like");
|
||||||
}
|
}
|
||||||
|
|
||||||
return role;
|
return like;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get id(): string {
|
public get id(): string {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Status } from "@versia/client/schemas";
|
||||||
import { db, Instance } from "@versia/kit/db";
|
import { db, Instance } from "@versia/kit/db";
|
||||||
import {
|
import {
|
||||||
EmojiToNote,
|
EmojiToNote,
|
||||||
|
Likes,
|
||||||
MediasToNotes,
|
MediasToNotes,
|
||||||
Notes,
|
Notes,
|
||||||
NoteToMentions,
|
NoteToMentions,
|
||||||
|
|
@ -49,9 +50,6 @@ type NoteTypeWithRelations = NoteType & {
|
||||||
reply: NoteType | null;
|
reply: NoteType | null;
|
||||||
quote: NoteType | null;
|
quote: NoteType | null;
|
||||||
application: typeof Application.$type | null;
|
application: typeof Application.$type | null;
|
||||||
reblogCount: number;
|
|
||||||
likeCount: number;
|
|
||||||
replyCount: number;
|
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
reblogged: boolean;
|
reblogged: boolean;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
|
@ -104,6 +102,16 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
throw new Error("Failed to insert status");
|
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;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,6 +316,24 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
return this.author.local;
|
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
|
* Updates the emojis associated with this note in the database
|
||||||
*
|
*
|
||||||
|
|
@ -531,12 +557,11 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(ids?: string[]): Promise<void> {
|
public async delete(): 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));
|
await db.delete(Notes).where(eq(Notes.id, this.id));
|
||||||
}
|
|
||||||
|
// Update author's status count
|
||||||
|
await this.author.recalculateStatusCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update(
|
public async update(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import {
|
import {
|
||||||
transformOutputToUserWithRelations,
|
transformOutputToUserWithRelations,
|
||||||
userExtrasTemplate,
|
|
||||||
userRelations,
|
userRelations,
|
||||||
} from "../functions/user.ts";
|
} from "../functions/user.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
@ -78,7 +77,6 @@ export class Notification extends BaseInterface<
|
||||||
with: {
|
with: {
|
||||||
...userRelations,
|
...userRelations,
|
||||||
},
|
},
|
||||||
extras: userExtrasTemplate("Notifications_account"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -112,7 +110,6 @@ export class Notification extends BaseInterface<
|
||||||
with: {
|
with: {
|
||||||
...userRelations,
|
...userRelations,
|
||||||
},
|
},
|
||||||
extras: userExtrasTemplate("Notifications_account"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extras: extra?.extras,
|
extras: extra?.extras,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||||
import { db } from "@versia/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Relationships } from "@versia/kit/tables";
|
import { Relationships, Users } from "@versia/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import {
|
import {
|
||||||
and,
|
and,
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
inArray,
|
inArray,
|
||||||
type SQL,
|
type SQL,
|
||||||
|
sql,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
@ -246,6 +247,40 @@ export class Relationship extends BaseInterface<
|
||||||
.set(newRelationship)
|
.set(newRelationship)
|
||||||
.where(eq(Relationships.id, this.id));
|
.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);
|
const updated = await Relationship.fromId(this.data.id);
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Notes,
|
Notes,
|
||||||
NoteToMentions,
|
NoteToMentions,
|
||||||
Notifications,
|
Notifications,
|
||||||
|
Relationships,
|
||||||
Users,
|
Users,
|
||||||
UserToPinnedNotes,
|
UserToPinnedNotes,
|
||||||
} from "@versia/kit/tables";
|
} from "@versia/kit/tables";
|
||||||
|
|
@ -203,6 +204,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
languages: options?.languages,
|
languages: options?.languages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!otherUser.data.isLocked) {
|
||||||
|
// Update the follower count
|
||||||
|
await otherUser.recalculateFollowerCount();
|
||||||
|
await this.recalculateFollowingCount();
|
||||||
|
}
|
||||||
|
|
||||||
if (otherUser.remote) {
|
if (otherUser.remote) {
|
||||||
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
await deliveryQueue.add(DeliveryJobType.FederateEntity, {
|
||||||
entity: {
|
entity: {
|
||||||
|
|
@ -237,6 +244,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.recalculateFollowingCount();
|
||||||
|
await followee.recalculateFollowerCount();
|
||||||
|
|
||||||
await relationship.update({
|
await relationship.update({
|
||||||
following: false,
|
following: false,
|
||||||
});
|
});
|
||||||
|
|
@ -262,6 +272,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
throw new Error("Followee must be a local user");
|
throw new Error("Followee must be a local user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await follower.recalculateFollowerCount();
|
||||||
|
await this.recalculateFollowingCount();
|
||||||
|
|
||||||
const entity = new VersiaEntities.FollowAccept({
|
const entity = new VersiaEntities.FollowAccept({
|
||||||
type: "FollowAccept",
|
type: "FollowAccept",
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
|
|
@ -504,6 +517,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
uri: uri?.href,
|
uri: uri?.href,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await note.recalculateReblogCount();
|
||||||
|
|
||||||
// Refetch the note *again* to get the proper value of .reblogged
|
// Refetch the note *again* to get the proper value of .reblogged
|
||||||
const finalNewReblog = await Note.fromId(newReblog.id, this?.id);
|
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 reblogToDelete.delete();
|
||||||
|
|
||||||
|
await note.recalculateReblogCount();
|
||||||
|
|
||||||
if (note.author.local) {
|
if (note.author.local) {
|
||||||
// Remove any eventual notifications for this reblog
|
// Remove any eventual notifications for this reblog
|
||||||
await db
|
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.
|
* Like a note.
|
||||||
*
|
*
|
||||||
|
|
@ -611,6 +667,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
uri: uri?.href,
|
uri: uri?.href,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await note.recalculateLikeCount();
|
||||||
|
|
||||||
if (note.author.local) {
|
if (note.author.local) {
|
||||||
// Notify the user that their post has been favourited
|
// Notify the user that their post has been favourited
|
||||||
await note.author.notify("favourite", this, note);
|
await note.author.notify("favourite", this, note);
|
||||||
|
|
@ -650,6 +708,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
await likeToDelete.delete();
|
await likeToDelete.delete();
|
||||||
|
|
||||||
|
await note.recalculateLikeCount();
|
||||||
|
|
||||||
if (note.author.local) {
|
if (note.author.local) {
|
||||||
// Remove any eventual notifications for this like
|
// Remove any eventual notifications for this like
|
||||||
await likeToDelete.clearRelatedNotifications();
|
await likeToDelete.clearRelatedNotifications();
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,7 @@ import { mentionValidator } from "@/api";
|
||||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
import type * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
import type * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
||||||
import {
|
import { transformOutputToUserWithRelations, userRelations } from "./user.ts";
|
||||||
transformOutputToUserWithRelations,
|
|
||||||
userExtrasTemplate,
|
|
||||||
userRelations,
|
|
||||||
} from "./user.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper against the Status object to make it easier to work with
|
* Wrapper against the Status object to make it easier to work with
|
||||||
|
|
@ -58,7 +54,6 @@ export const findManyNotes = async (
|
||||||
with: {
|
with: {
|
||||||
...userRelations,
|
...userRelations,
|
||||||
},
|
},
|
||||||
extras: userExtrasTemplate("Notes_author"),
|
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
with: {
|
with: {
|
||||||
|
|
@ -92,9 +87,6 @@ export const findManyNotes = async (
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
with: userRelations,
|
with: userRelations,
|
||||||
extras: userExtrasTemplate(
|
|
||||||
"Notes_reblog_mentions_user",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -102,22 +94,9 @@ export const findManyNotes = async (
|
||||||
with: {
|
with: {
|
||||||
...userRelations,
|
...userRelations,
|
||||||
},
|
},
|
||||||
extras: userExtrasTemplate("Notes_reblog_author"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extras: {
|
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
|
pinned: userId
|
||||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||||
"pinned",
|
"pinned",
|
||||||
|
|
@ -144,18 +123,6 @@ export const findManyNotes = async (
|
||||||
quote: true,
|
quote: true,
|
||||||
},
|
},
|
||||||
extras: {
|
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
|
pinned: userId
|
||||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
||||||
"pinned",
|
"pinned",
|
||||||
|
|
@ -200,17 +167,11 @@ export const findManyNotes = async (
|
||||||
(attachment) => attachment.media,
|
(attachment) => attachment.media,
|
||||||
),
|
),
|
||||||
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
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),
|
pinned: Boolean(post.reblog.pinned),
|
||||||
reblogged: Boolean(post.reblog.reblogged),
|
reblogged: Boolean(post.reblog.reblogged),
|
||||||
muted: Boolean(post.reblog.muted),
|
muted: Boolean(post.reblog.muted),
|
||||||
liked: Boolean(post.reblog.liked),
|
liked: Boolean(post.reblog.liked),
|
||||||
},
|
},
|
||||||
reblogCount: Number(post.reblogCount),
|
|
||||||
likeCount: Number(post.likeCount),
|
|
||||||
replyCount: Number(post.replyCount),
|
|
||||||
pinned: Boolean(post.pinned),
|
pinned: Boolean(post.pinned),
|
||||||
reblogged: Boolean(post.reblogged),
|
reblogged: Boolean(post.reblogged),
|
||||||
muted: Boolean(post.muted),
|
muted: Boolean(post.muted),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
type User,
|
type User,
|
||||||
} from "@versia/kit/db";
|
} from "@versia/kit/db";
|
||||||
import type { Users } from "@versia/kit/tables";
|
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 = {
|
export const userRelations = {
|
||||||
instance: true,
|
instance: true,
|
||||||
|
|
@ -32,42 +32,6 @@ export const userRelations = {
|
||||||
},
|
},
|
||||||
} as const;
|
} 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 {
|
export interface AuthData {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
token: Token | null;
|
token: Token | null;
|
||||||
|
|
@ -131,10 +95,6 @@ export const findManyUsers = async (
|
||||||
...userRelations,
|
...userRelations,
|
||||||
...query?.with,
|
...query?.with,
|
||||||
},
|
},
|
||||||
extras: {
|
|
||||||
...userExtras,
|
|
||||||
...query?.extras,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
case "pub.versia:shares/Share": {
|
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,
|
"when": 1744979203068,
|
||||||
"tag": "0049_graceful_iron_man",
|
"tag": "0049_graceful_iron_man",
|
||||||
"breakpoints": true
|
"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")
|
visibility: text("visibility")
|
||||||
.$type<z.infer<typeof StatusSchema.shape.visibility>>()
|
.$type<z.infer<typeof StatusSchema.shape.visibility>>()
|
||||||
.notNull(),
|
.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, {
|
replyId: uuid("replyId").references((): AnyPgColumn => Notes.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
|
|
@ -583,6 +586,9 @@ export const Users = pgTable(
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
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(),
|
createdAt: createdAt(),
|
||||||
updatedAt: updatedAt(),
|
updatedAt: updatedAt(),
|
||||||
isBot: boolean("is_bot").default(false).notNull(),
|
isBot: boolean("is_bot").default(false).notNull(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue