ref: improve friendship endpoints
This commit is contained in:
parent
aaff5dcaf4
commit
b8bc0734a0
@ -0,0 +1,26 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RelationshipStatus" AS ENUM ('pending', 'accepted', 'rejected', 'removed');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserRelationship" (
|
||||
"fromUserId" TEXT NOT NULL,
|
||||
"toUserId" TEXT NOT NULL,
|
||||
"status" "RelationshipStatus" NOT NULL DEFAULT 'pending',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"acceptedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "UserRelationship_pkey" PRIMARY KEY ("fromUserId","toUserId")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserRelationship_toUserId_status_idx" ON "UserRelationship"("toUserId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserRelationship_fromUserId_status_idx" ON "UserRelationship"("fromUserId", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserRelationship" ADD CONSTRAINT "UserRelationship_fromUserId_fkey" FOREIGN KEY ("fromUserId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserRelationship" ADD CONSTRAINT "UserRelationship_toUserId_fkey" FOREIGN KEY ("toUserId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -9,6 +9,8 @@ import { db } from "@/storage/db";
|
||||
import { RelationshipStatus } from "@prisma/client";
|
||||
|
||||
// Shared Zod Schemas
|
||||
|
||||
const RelationshipStatusSchema = z.enum(['pending', 'accepted', 'rejected', 'removed']);
|
||||
const UserProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
firstName: z.string(),
|
||||
@ -20,11 +22,10 @@ const UserProfileSchema = z.object({
|
||||
height: z.number().optional(),
|
||||
thumbhash: z.string().optional()
|
||||
}).nullable(),
|
||||
username: z.string()
|
||||
username: z.string(),
|
||||
status: RelationshipStatusSchema
|
||||
});
|
||||
|
||||
const RelationshipStatusSchema = z.enum(['pending', 'accepted', 'rejected', 'removed']);
|
||||
|
||||
const RelationshipSchema = z.object({
|
||||
fromUserId: z.string(),
|
||||
toUserId: z.string(),
|
||||
@ -37,7 +38,7 @@ const RelationshipSchema = z.object({
|
||||
export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
|
||||
// Get multiple user profiles
|
||||
app.get('/v1/friends/profiles', {
|
||||
app.get('/v1/profiles', {
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
userIds: z.string() // Comma-separated list
|
||||
@ -54,7 +55,7 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
const userIdArray = userIds.split(',').filter(id => id.trim());
|
||||
|
||||
try {
|
||||
const profiles = await FriendshipService.getUserProfiles(userIdArray);
|
||||
const profiles = await FriendshipService.getUserProfiles(userIdArray, request.userId);
|
||||
return reply.send({ profiles });
|
||||
} catch (error) {
|
||||
log({ module: 'api', level: 'error' }, `Failed to get profiles: ${error}`);
|
||||
@ -63,7 +64,7 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
});
|
||||
|
||||
// Search user by username
|
||||
app.get('/v1/friends/search', {
|
||||
app.get('/v1/profiles/search', {
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
username: z.string()
|
||||
@ -79,7 +80,7 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
const { username } = request.query;
|
||||
|
||||
try {
|
||||
const profile = await FriendshipService.searchUserByUsername(username);
|
||||
const profile = await FriendshipService.searchUserByUsername(username, request.userId);
|
||||
return reply.send({ profile });
|
||||
} catch (error) {
|
||||
log({ module: 'api', level: 'error' }, `Failed to search user: ${error}`);
|
||||
@ -94,9 +95,8 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
recipientId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: RelationshipSchema,
|
||||
400: z.object({ error: z.string() }),
|
||||
403: z.object({ error: z.string() })
|
||||
200: z.object({ profile: UserProfileSchema.nullable() }),
|
||||
400: z.object({ error: z.string() })
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
@ -116,23 +116,24 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
]);
|
||||
|
||||
if (!hasGitHub || !recipientHasGitHub) {
|
||||
return reply.code(403).send({ error: 'Both users must have GitHub connected' });
|
||||
return reply.send({ profile: null });
|
||||
}
|
||||
|
||||
const relationship = await FriendshipService.sendFriendRequest(userId, recipientId);
|
||||
const profile = await FriendshipService.sendFriendRequest(userId, recipientId);
|
||||
|
||||
// Get profiles for the socket event
|
||||
const [fromUserProfile, toUserProfile] = await FriendshipService.getUserProfiles([userId, recipientId]);
|
||||
// Get profiles (relative to recipient) for the socket event
|
||||
const [fromUserProfile, toUserProfile] = await FriendshipService.getUserProfiles([userId, recipientId], recipientId);
|
||||
|
||||
// Emit socket event to recipient
|
||||
const updateSeq = await allocateUserSeq(recipientId);
|
||||
const updatePayload = buildRelationshipUpdatedEvent(
|
||||
{
|
||||
fromUserId: relationship.fromUserId,
|
||||
toUserId: relationship.toUserId,
|
||||
status: relationship.status,
|
||||
fromUserId: userId,
|
||||
toUserId: recipientId,
|
||||
status: 'pending',
|
||||
action: 'created',
|
||||
fromUser: fromUserProfile,
|
||||
toUser: toUserProfile,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
updateSeq,
|
||||
@ -145,20 +146,10 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
recipientFilter: { type: 'user-scoped-only' }
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
fromUserId: relationship.fromUserId,
|
||||
toUserId: relationship.toUserId,
|
||||
status: relationship.status as any,
|
||||
createdAt: relationship.createdAt.toISOString(),
|
||||
updatedAt: relationship.updatedAt.toISOString(),
|
||||
acceptedAt: relationship.acceptedAt?.toISOString() || null
|
||||
});
|
||||
} catch (error: any) {
|
||||
return reply.send({ profile });
|
||||
} catch (error) {
|
||||
log({ module: 'api', level: 'error' }, `Failed to send friend request: ${error}`);
|
||||
if (error.message === 'Friend request already exists') {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
return reply.code(500).send({ error: 'Failed to send friend request' });
|
||||
return reply.send({ profile: null });
|
||||
}
|
||||
});
|
||||
|
||||
@ -171,12 +162,8 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
accept: z.boolean()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
relationship: RelationshipSchema,
|
||||
reverseRelationship: RelationshipSchema.optional()
|
||||
}),
|
||||
400: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() })
|
||||
200: z.object({ profile: UserProfileSchema.nullable() }),
|
||||
400: z.object({ error: z.string() })
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
@ -191,19 +178,17 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
|
||||
try {
|
||||
if (accept) {
|
||||
const result = await FriendshipService.acceptFriendRequest(fromUserId, toUserId);
|
||||
const profile = await FriendshipService.acceptFriendRequest(fromUserId, toUserId);
|
||||
|
||||
// Get profiles for the socket event
|
||||
const [fromUserProfile, toUserProfile] = await FriendshipService.getUserProfiles([fromUserId, toUserId]);
|
||||
|
||||
// Emit socket event to both users
|
||||
// Emit socket event to both users with status accepted
|
||||
for (const targetUserId of [fromUserId, toUserId]) {
|
||||
const [fromUserProfile, toUserProfile] = await FriendshipService.getUserProfiles([fromUserId, toUserId], targetUserId);
|
||||
const updateSeq = await allocateUserSeq(targetUserId);
|
||||
const updatePayload = buildRelationshipUpdatedEvent(
|
||||
{
|
||||
fromUserId: result.relationship.fromUserId,
|
||||
toUserId: result.relationship.toUserId,
|
||||
status: result.relationship.status,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
status: 'accepted',
|
||||
action: 'updated',
|
||||
fromUser: fromUserProfile,
|
||||
toUser: toUserProfile,
|
||||
@ -220,46 +205,15 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
relationship: {
|
||||
fromUserId: result.relationship.fromUserId,
|
||||
toUserId: result.relationship.toUserId,
|
||||
status: result.relationship.status as any,
|
||||
createdAt: result.relationship.createdAt.toISOString(),
|
||||
updatedAt: result.relationship.updatedAt.toISOString(),
|
||||
acceptedAt: result.relationship.acceptedAt?.toISOString() || null
|
||||
},
|
||||
reverseRelationship: {
|
||||
fromUserId: result.reverseRelationship.fromUserId,
|
||||
toUserId: result.reverseRelationship.toUserId,
|
||||
status: result.reverseRelationship.status as any,
|
||||
createdAt: result.reverseRelationship.createdAt.toISOString(),
|
||||
updatedAt: result.reverseRelationship.updatedAt.toISOString(),
|
||||
acceptedAt: result.reverseRelationship.acceptedAt?.toISOString() || null
|
||||
}
|
||||
});
|
||||
return reply.send({ profile });
|
||||
} else {
|
||||
const relationship = await FriendshipService.rejectFriendRequest(fromUserId, toUserId);
|
||||
|
||||
const profile = await FriendshipService.rejectFriendRequest(fromUserId, toUserId);
|
||||
// No socket event for rejections (hidden from requestor)
|
||||
|
||||
return reply.send({
|
||||
relationship: {
|
||||
fromUserId: relationship.fromUserId,
|
||||
toUserId: relationship.toUserId,
|
||||
status: relationship.status as any,
|
||||
createdAt: relationship.createdAt.toISOString(),
|
||||
updatedAt: relationship.updatedAt.toISOString(),
|
||||
acceptedAt: relationship.acceptedAt?.toISOString() || null
|
||||
}
|
||||
});
|
||||
return reply.send({ profile });
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
log({ module: 'api', level: 'error' }, `Failed to respond to friend request: ${error}`);
|
||||
if (error.message === 'No pending friend request found') {
|
||||
return reply.code(404).send({ error: error.message });
|
||||
}
|
||||
return reply.code(500).send({ error: 'Failed to respond to friend request' });
|
||||
return reply.send({ profile: null });
|
||||
}
|
||||
});
|
||||
|
||||
@ -329,9 +283,7 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
friendId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
removed: z.boolean()
|
||||
})
|
||||
200: z.object({ profile: UserProfileSchema.nullable() })
|
||||
}
|
||||
},
|
||||
preHandler: app.authenticate
|
||||
@ -340,10 +292,10 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
const { friendId } = request.params;
|
||||
|
||||
try {
|
||||
const removed = await FriendshipService.removeFriend(userId, friendId);
|
||||
const profile = await FriendshipService.removeFriend(userId, friendId);
|
||||
|
||||
// Get profiles for the socket event
|
||||
const [userProfile] = await FriendshipService.getUserProfiles([userId]);
|
||||
const [userProfile] = await FriendshipService.getUserProfiles([userId], friendId);
|
||||
|
||||
// Emit socket event to the friend
|
||||
const updateSeq = await allocateUserSeq(friendId);
|
||||
@ -366,10 +318,10 @@ export function friendshipRoutes(app: Fastify, eventRouter: EventRouter) {
|
||||
recipientFilter: { type: 'user-scoped-only' }
|
||||
});
|
||||
|
||||
return reply.send({ removed });
|
||||
return reply.send({ profile });
|
||||
} catch (error) {
|
||||
log({ module: 'api', level: 'error' }, `Failed to remove friend: ${error}`);
|
||||
return reply.code(500).send({ removed: false });
|
||||
return reply.send({ profile: null });
|
||||
}
|
||||
});
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { db } from "@/storage/db";
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { Account, RelationshipStatus, UserRelationship } from "@prisma/client";
|
||||
import { getPublicUrl } from "@/storage/files";
|
||||
import { GitHubProfile } from "@/app/api/types";
|
||||
@ -15,36 +16,69 @@ export interface UserProfile {
|
||||
thumbhash?: string;
|
||||
} | null;
|
||||
username: string;
|
||||
status: RelationshipStatus;
|
||||
}
|
||||
|
||||
export class FriendshipService {
|
||||
private static isImageRefLike(value: unknown): value is { path: string; width?: number; height?: number; thumbhash?: string } {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
return typeof v.path === 'string';
|
||||
}
|
||||
|
||||
private static client(client?: Prisma.TransactionClient | PrismaClient) {
|
||||
return client ?? db;
|
||||
}
|
||||
|
||||
private static async getStatusBetween(a: string, b: string, client?: Prisma.TransactionClient | PrismaClient): Promise<RelationshipStatus> {
|
||||
const c = this.client(client);
|
||||
const rels = await c.userRelationship.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ fromUserId: a, toUserId: b },
|
||||
{ fromUserId: b, toUserId: a }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (rels.some(r => r.status === RelationshipStatus.accepted)) return RelationshipStatus.accepted;
|
||||
if (rels.some(r => r.status === RelationshipStatus.pending)) return RelationshipStatus.pending;
|
||||
if (rels.some(r => r.status === RelationshipStatus.rejected)) return RelationshipStatus.rejected;
|
||||
return RelationshipStatus.removed;
|
||||
}
|
||||
/**
|
||||
* Build user profile from account data
|
||||
*/
|
||||
static buildUserProfile(account: Account & {
|
||||
githubUser?: {
|
||||
profile: any;
|
||||
} | null;
|
||||
avatar?: any;
|
||||
}): UserProfile {
|
||||
const githubProfile = account.githubUser?.profile as GitHubProfile | undefined;
|
||||
static buildUserProfile(
|
||||
account: Account & { githubUser?: { profile: GitHubProfile } | null },
|
||||
status: RelationshipStatus = RelationshipStatus.removed
|
||||
): UserProfile {
|
||||
const githubProfile = account.githubUser?.profile;
|
||||
const avatarJson = account.avatar as Prisma.JsonValue | null;
|
||||
let avatar: UserProfile['avatar'] = null;
|
||||
if (this.isImageRefLike(avatarJson)) {
|
||||
avatar = {
|
||||
path: avatarJson.path,
|
||||
url: getPublicUrl(avatarJson.path),
|
||||
width: avatarJson.width,
|
||||
height: avatarJson.height,
|
||||
thumbhash: avatarJson.thumbhash
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
firstName: account.firstName || '',
|
||||
lastName: account.lastName,
|
||||
avatar: account.avatar ? {
|
||||
...account.avatar,
|
||||
url: getPublicUrl(account.avatar.path)
|
||||
} : null,
|
||||
username: githubProfile?.login || ''
|
||||
avatar,
|
||||
username: githubProfile?.login || '',
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple user profiles by IDs
|
||||
*/
|
||||
static async getUserProfiles(userIds: string[]): Promise<UserProfile[]> {
|
||||
static async getUserProfiles(userIds: string[], relativeToUserId?: string): Promise<UserProfile[]> {
|
||||
if (userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@ -59,13 +93,37 @@ export class FriendshipService {
|
||||
}
|
||||
});
|
||||
|
||||
return accounts.map(account => this.buildUserProfile(account));
|
||||
let statusMap: Record<string, RelationshipStatus> = {};
|
||||
if (relativeToUserId) {
|
||||
const rels = await db.userRelationship.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ fromUserId: relativeToUserId, toUserId: { in: userIds } },
|
||||
{ fromUserId: { in: userIds }, toUserId: relativeToUserId }
|
||||
]
|
||||
}
|
||||
});
|
||||
const tmp: Record<string, RelationshipStatus[]> = {};
|
||||
for (const r of rels) {
|
||||
const otherId = r.fromUserId === relativeToUserId ? r.toUserId : r.fromUserId;
|
||||
(tmp[otherId] ||= []).push(r.status);
|
||||
}
|
||||
for (const [uid, statuses] of Object.entries(tmp)) {
|
||||
let s: RelationshipStatus = RelationshipStatus.removed;
|
||||
if (statuses.includes(RelationshipStatus.accepted)) s = RelationshipStatus.accepted;
|
||||
else if (statuses.includes(RelationshipStatus.pending)) s = RelationshipStatus.pending;
|
||||
else if (statuses.includes(RelationshipStatus.rejected)) s = RelationshipStatus.rejected;
|
||||
statusMap[uid] = s;
|
||||
}
|
||||
}
|
||||
|
||||
return accounts.map(account => this.buildUserProfile(account, statusMap[account.id] ?? RelationshipStatus.removed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a user by exact username match
|
||||
*/
|
||||
static async searchUserByUsername(username: string): Promise<UserProfile | null> {
|
||||
static async searchUserByUsername(username: string, relativeToUserId?: string): Promise<UserProfile | null> {
|
||||
const githubUser = await db.githubUser.findFirst({
|
||||
where: {
|
||||
profile: {
|
||||
@ -92,13 +150,14 @@ export class FriendshipService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.buildUserProfile(account);
|
||||
const status = relativeToUserId ? await this.getStatusBetween(relativeToUserId, account.id) : RelationshipStatus.removed;
|
||||
return this.buildUserProfile(account, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a friend request from one user to another
|
||||
*/
|
||||
static async sendFriendRequest(fromUserId: string, toUserId: string): Promise<UserRelationship> {
|
||||
static async sendFriendRequest(fromUserId: string, toUserId: string): Promise<UserProfile | null> {
|
||||
// Verify both users exist and have GitHub connected
|
||||
const [fromUser, toUser] = await Promise.all([
|
||||
db.account.findFirst({
|
||||
@ -110,55 +169,59 @@ export class FriendshipService {
|
||||
]);
|
||||
|
||||
if (!fromUser || !toUser) {
|
||||
throw new Error('Both users must exist and have GitHub connected');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if relationship already exists
|
||||
const existing = await db.userRelationship.findUnique({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (existing.status === RelationshipStatus.rejected) {
|
||||
// Allow re-sending if previously rejected
|
||||
return await db.userRelationship.update({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: RelationshipStatus.pending,
|
||||
updatedAt: new Date()
|
||||
// Interactive transaction to avoid races between check and write
|
||||
const created = await db.$transaction(async (tx) => {
|
||||
const existing = await tx.userRelationship.findUnique({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error('Friend request already exists');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create new friend request
|
||||
return await db.userRelationship.create({
|
||||
data: {
|
||||
fromUserId,
|
||||
toUserId,
|
||||
status: RelationshipStatus.pending
|
||||
if (existing) {
|
||||
if (existing.status === RelationshipStatus.rejected) {
|
||||
// Allow re-sending if previously rejected
|
||||
return await tx.userRelationship.update({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: RelationshipStatus.pending,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new friend request
|
||||
return await tx.userRelationship.create({
|
||||
data: {
|
||||
fromUserId,
|
||||
toUserId,
|
||||
status: RelationshipStatus.pending
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!created) return null;
|
||||
const account = await db.account.findUnique({ where: { id: toUserId }, include: { githubUser: true } });
|
||||
if (!account) return null;
|
||||
const status = await this.getStatusBetween(fromUserId, toUserId);
|
||||
return this.buildUserProfile(account, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a friend request
|
||||
*/
|
||||
static async acceptFriendRequest(fromUserId: string, toUserId: string): Promise<{
|
||||
relationship: UserRelationship;
|
||||
reverseRelationship: UserRelationship;
|
||||
}> {
|
||||
static async acceptFriendRequest(fromUserId: string, toUserId: string): Promise<UserProfile | null> {
|
||||
// Verify the request exists and is pending
|
||||
const request = await db.userRelationship.findUnique({
|
||||
where: {
|
||||
@ -170,11 +233,11 @@ export class FriendshipService {
|
||||
});
|
||||
|
||||
if (!request || request.status !== RelationshipStatus.pending) {
|
||||
throw new Error('No pending friend request found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use transaction to ensure both operations succeed
|
||||
const result = await db.$transaction(async (tx) => {
|
||||
const ok = await db.$transaction(async (tx) => {
|
||||
// Update original request to accepted
|
||||
const relationship = await tx.userRelationship.update({
|
||||
where: {
|
||||
@ -199,58 +262,71 @@ export class FriendshipService {
|
||||
}
|
||||
});
|
||||
|
||||
return { relationship, reverseRelationship };
|
||||
return !!relationship && !!reverseRelationship;
|
||||
});
|
||||
|
||||
return result;
|
||||
if (!ok) return null;
|
||||
const account = await db.account.findUnique({ where: { id: fromUserId }, include: { githubUser: true } });
|
||||
if (!account) return null;
|
||||
const status = await this.getStatusBetween(toUserId, fromUserId);
|
||||
return this.buildUserProfile(account, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a friend request
|
||||
*/
|
||||
static async rejectFriendRequest(fromUserId: string, toUserId: string): Promise<UserRelationship> {
|
||||
const request = await db.userRelationship.findUnique({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
static async rejectFriendRequest(fromUserId: string, toUserId: string): Promise<UserProfile | null> {
|
||||
return await db.$transaction(async (tx) => {
|
||||
const request = await tx.userRelationship.findUnique({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!request || request.status !== RelationshipStatus.pending) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!request || request.status !== RelationshipStatus.pending) {
|
||||
throw new Error('No pending friend request found');
|
||||
}
|
||||
|
||||
return await db.userRelationship.update({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
const _ = await tx.userRelationship.update({
|
||||
where: {
|
||||
fromUserId_toUserId: {
|
||||
fromUserId,
|
||||
toUserId
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: RelationshipStatus.rejected
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: RelationshipStatus.rejected
|
||||
}
|
||||
});
|
||||
const account = await tx.account.findUnique({ where: { id: fromUserId }, include: { githubUser: true } });
|
||||
if (!account) return null;
|
||||
const status = await this.getStatusBetween(toUserId, fromUserId, tx);
|
||||
return this.buildUserProfile(account, status);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a friendship (both directions)
|
||||
*/
|
||||
static async removeFriend(userId: string, friendId: string): Promise<boolean> {
|
||||
await db.$transaction([
|
||||
db.userRelationship.deleteMany({
|
||||
static async removeFriend(userId: string, friendId: string): Promise<UserProfile | null> {
|
||||
const ok = await db.$transaction(async (tx) => {
|
||||
await tx.userRelationship.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ fromUserId: userId, toUserId: friendId },
|
||||
{ fromUserId: friendId, toUserId: userId }
|
||||
]
|
||||
}
|
||||
})
|
||||
]);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
if (!ok) return null;
|
||||
const account = await db.account.findUnique({ where: { id: friendId }, include: { githubUser: true } });
|
||||
if (!account) return null;
|
||||
return this.buildUserProfile(account, RelationshipStatus.removed);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -278,7 +354,7 @@ export class FriendshipService {
|
||||
|
||||
return requests.map(request => ({
|
||||
...request,
|
||||
fromUser: this.buildUserProfile(request.fromUser)
|
||||
fromUser: this.buildUserProfile(request.fromUser, RelationshipStatus.pending)
|
||||
}));
|
||||
}
|
||||
|
||||
@ -334,7 +410,7 @@ export class FriendshipService {
|
||||
}
|
||||
});
|
||||
|
||||
return friends.map(friend => this.buildUserProfile(friend));
|
||||
return friends.map(friend => this.buildUserProfile(friend, RelationshipStatus.accepted));
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user