mirror of
https://github.com/versia-pub/server.git
synced 2026-04-27 20:59:15 +02:00
refactor: ♻️ Rewrite build system to fit the monorepo architecture
This commit is contained in:
parent
7de4b573e3
commit
90b6399407
217 changed files with 2143 additions and 1858 deletions
353
packages/kit/db/relationship.ts
Normal file
353
packages/kit/db/relationship.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
type SQL,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db } from "../tables/db.ts";
|
||||
import { Relationships, Users } from "../tables/schema.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
import type { User } from "./user.ts";
|
||||
|
||||
type RelationshipType = InferSelectModel<typeof Relationships>;
|
||||
|
||||
type RelationshipWithOpposite = RelationshipType & {
|
||||
followedBy: boolean;
|
||||
blockedBy: boolean;
|
||||
requestedBy: boolean;
|
||||
};
|
||||
|
||||
export class Relationship extends BaseInterface<
|
||||
typeof Relationships,
|
||||
RelationshipWithOpposite
|
||||
> {
|
||||
public static schema = z.object({
|
||||
id: z.string(),
|
||||
blocked_by: z.boolean(),
|
||||
blocking: z.boolean(),
|
||||
domain_blocking: z.boolean(),
|
||||
endorsed: z.boolean(),
|
||||
followed_by: z.boolean(),
|
||||
following: z.boolean(),
|
||||
muting_notifications: z.boolean(),
|
||||
muting: z.boolean(),
|
||||
note: z.string().nullable(),
|
||||
notifying: z.boolean(),
|
||||
requested_by: z.boolean(),
|
||||
requested: z.boolean(),
|
||||
showing_reblogs: z.boolean(),
|
||||
});
|
||||
|
||||
public static $type: RelationshipWithOpposite;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Relationship.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload relationship");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(
|
||||
id: string | null,
|
||||
): Promise<Relationship | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Relationship.fromSql(eq(Relationships.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Relationship[]> {
|
||||
return await Relationship.manyFromSql(inArray(Relationships.id, ids));
|
||||
}
|
||||
|
||||
public static async fromOwnerAndSubject(
|
||||
owner: User,
|
||||
subject: User,
|
||||
): Promise<Relationship> {
|
||||
const found = await Relationship.fromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
eq(Relationships.subjectId, subject.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
// Create a new relationship if one doesn't exist
|
||||
return await Relationship.insert({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: owner.id,
|
||||
subjectId: subject.id,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
});
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
public static async fromOwnerAndSubjects(
|
||||
owner: User,
|
||||
subjectIds: string[],
|
||||
): Promise<Relationship[]> {
|
||||
const found = await Relationship.manyFromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
inArray(Relationships.subjectId, subjectIds),
|
||||
),
|
||||
);
|
||||
|
||||
const missingSubjectsIds = subjectIds.filter(
|
||||
(id) => !found.find((rel) => rel.data.subjectId === id),
|
||||
);
|
||||
|
||||
for (const subjectId of missingSubjectsIds) {
|
||||
await Relationship.insert({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: owner.id,
|
||||
subjectId,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
});
|
||||
}
|
||||
|
||||
return await Relationship.manyFromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
inArray(Relationships.subjectId, subjectIds),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
|
||||
): Promise<Relationship | null> {
|
||||
const found = await db.query.Relationships.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opposite = await Relationship.getOpposite(found);
|
||||
|
||||
return new Relationship({
|
||||
...found,
|
||||
followedBy: opposite.following,
|
||||
blockedBy: opposite.blocking,
|
||||
requestedBy: opposite.requested,
|
||||
});
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Relationships.findMany>[0],
|
||||
): Promise<Relationship[]> {
|
||||
const found = await db.query.Relationships.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
const opposites = await Promise.all(
|
||||
found.map((rel) => Relationship.getOpposite(rel)),
|
||||
);
|
||||
|
||||
return found.map((s, i) => {
|
||||
return new Relationship({
|
||||
...s,
|
||||
followedBy: opposites[i].following,
|
||||
blockedBy: opposites[i].blocking,
|
||||
requestedBy: opposites[i].requested,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static async getOpposite(oppositeTo: {
|
||||
subjectId: string;
|
||||
ownerId: string;
|
||||
}): Promise<RelationshipType> {
|
||||
let output = await db.query.Relationships.findFirst({
|
||||
where: (rel): SQL | undefined =>
|
||||
and(
|
||||
eq(rel.ownerId, oppositeTo.subjectId),
|
||||
eq(rel.subjectId, oppositeTo.ownerId),
|
||||
),
|
||||
});
|
||||
|
||||
// If the opposite relationship doesn't exist, create it
|
||||
if (!output) {
|
||||
output = (
|
||||
await db
|
||||
.insert(Relationships)
|
||||
.values({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: oppositeTo.subjectId,
|
||||
subjectId: oppositeTo.ownerId,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async update(
|
||||
newRelationship: Partial<RelationshipType>,
|
||||
): Promise<RelationshipWithOpposite> {
|
||||
await db
|
||||
.update(Relationships)
|
||||
.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) {
|
||||
throw new Error("Failed to update relationship");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<RelationshipWithOpposite> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db
|
||||
.delete(Relationships)
|
||||
.where(inArray(Relationships.id, ids));
|
||||
} else {
|
||||
await db.delete(Relationships).where(eq(Relationships.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Relationships>,
|
||||
): Promise<Relationship> {
|
||||
const inserted = (
|
||||
await db.insert(Relationships).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const relationship = await Relationship.fromId(inserted.id);
|
||||
|
||||
if (!relationship) {
|
||||
throw new Error("Failed to insert relationship");
|
||||
}
|
||||
|
||||
// Create opposite relationship if necessary
|
||||
await Relationship.getOpposite({
|
||||
subjectId: relationship.data.subjectId,
|
||||
ownerId: relationship.data.ownerId,
|
||||
});
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof RelationshipSchema> {
|
||||
return {
|
||||
id: this.data.subjectId,
|
||||
blocked_by: this.data.blockedBy,
|
||||
blocking: this.data.blocking,
|
||||
domain_blocking: this.data.domainBlocking,
|
||||
endorsed: this.data.endorsed,
|
||||
followed_by: this.data.followedBy,
|
||||
following: this.data.following,
|
||||
muting_notifications: this.data.mutingNotifications,
|
||||
muting: this.data.muting,
|
||||
note: this.data.note,
|
||||
notifying: this.data.notifying,
|
||||
requested_by: this.data.requestedBy,
|
||||
requested: this.data.requested,
|
||||
showing_reblogs: this.data.showingReblogs,
|
||||
languages: this.data.languages ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue