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

This commit is contained in:
Jesse Wierzbinski 2025-05-04 16:38:37 +02:00
parent cd12ccd6c1
commit ddb3cfc978
No known key found for this signature in database
16 changed files with 2676 additions and 106 deletions

View file

@ -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 () => {

View 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);
});
});

View file

@ -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);
});
}); });

View file

@ -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);
});
}); });

View file

@ -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 {

View file

@ -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(

View file

@ -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,

View file

@ -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) {

View file

@ -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();

View file

@ -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),

View file

@ -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));

View file

@ -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": {

View 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;

File diff suppressed because it is too large Load diff

View file

@ -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
} }
] ]
} }

View file

@ -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(),