Improve collaboration requests APIs
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-06-09 17:32:01 +03:00
الأصل 16de76d9eb
التزام ddac88582f
9 ملفات معدلة مع 454 إضافات و43 حذوفات

عرض الملف

@@ -2,6 +2,7 @@ import {
BadRequestException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
@@ -13,13 +14,18 @@ import { NotificationsService } from '../notifications/notifications.service';
import { PostsRepository } from '../posts/posts.repository';
import { UsersRepository } from '../users/users.repository';
import { CreateCollaborationRequestDto } from './dto/create-collaboration-request.dto';
import { CollaborationRequestQueryDto } from './dto/collaboration-request-query.dto';
import {
CollaborationRequest,
CollaborationRequestDocument,
CollaborationRequestStatus,
} from './schemas/collaboration-request.schema';
@Injectable()
export class CollaborationRequestsService {
private readonly logger = new Logger(CollaborationRequestsService.name);
private readonly userSelect = 'name username stageName avatar isVerified isDisabled';
constructor(
@InjectModel(CollaborationRequest.name)
private readonly collaborationRequestModel: Model<CollaborationRequestDocument>,
@@ -81,35 +87,96 @@ export class CollaborationRequestsService {
{ new: true, upsert: true, setDefaultsOnInsert: true },
)
.exec();
if (!request) {
throw new NotFoundException('Collaboration request not found');
}
if (!existing) {
await this.notificationsService.create({
await this.createCollaborationNotification({
actorId: requesterId,
recipientId: targetUserId,
type: 'collaboration_request',
referenceId: postId,
resourceType: 'post',
deepLink: `/posts/${postId}`,
postId,
requestId: request.id,
metadata: collaborationDetails,
});
}
return { message: 'Collaboration request sent', request };
return {
message: 'Collaboration request sent',
request: await this.findVisibleRequestOrFail(requesterId, request.id),
};
}
async getMine(targetUserId: string, query: PaginationQueryDto) {
return this.listReceived(targetUserId, { ...query, status: 'pending' });
}
async listReceived(targetUserId: string, query: CollaborationRequestQueryDto) {
return this.listForUser('targetUserId', targetUserId, query);
}
async listSent(requesterId: string, query: CollaborationRequestQueryDto) {
return this.listForUser('requesterId', requesterId, query);
}
async getById(currentUserId: string, requestId: string) {
return {
request: await this.findVisibleRequestOrFail(currentUserId, requestId),
};
}
async cancel(requesterId: string, requestId: string) {
if (!Types.ObjectId.isValid(requestId)) {
throw new BadRequestException('Invalid collaboration request id');
}
const request = await this.collaborationRequestModel.findById(requestId).exec();
if (!request) {
throw new NotFoundException('Collaboration request not found');
}
if (request.requesterId.toString() !== requesterId) {
throw new ForbiddenException('Only the requester can cancel this collaboration request');
}
if (request.status !== 'pending') {
throw new BadRequestException('Only pending collaboration requests can be cancelled');
}
request.status = 'cancelled';
await request.save();
await this.createCollaborationNotification({
actorId: requesterId,
recipientId: request.targetUserId.toString(),
type: 'collaboration_request_cancelled',
postId: request.postId.toString(),
requestId: request.id,
metadata: {
status: 'cancelled',
collaborationType: request.collaborationType ?? null,
},
});
return {
cancelled: true,
request: await this.findVisibleRequestOrFail(requesterId, request.id),
};
}
private async listForUser(
field: 'targetUserId' | 'requesterId',
userId: string,
query: CollaborationRequestQueryDto,
) {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const skip = (page - 1) * limit;
const filter = { targetUserId: new Types.ObjectId(targetUserId), status: 'pending' };
const filter: Record<string, unknown> = {
[field]: new Types.ObjectId(userId),
...(query.status ? { status: query.status } : {}),
};
const [items, total] = await Promise.all([
this.collaborationRequestModel
.find(filter)
.populate({
path: 'requesterId',
select: 'name username stageName avatar isVerified isDisabled',
})
.populate({ path: 'postId' })
this.populateRequestQuery(this.collaborationRequestModel.find(filter))
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
@@ -124,12 +191,42 @@ export class CollaborationRequestsService {
await this.postsRepository.updateById(request.postId.toString(), {
$addToSet: { collaboratorIds: request.targetUserId },
});
return { approved: true, request };
await this.createCollaborationNotification({
actorId: targetUserId,
recipientId: request.requesterId.toString(),
type: 'collaboration_request_approved',
postId: request.postId.toString(),
requestId: request.id,
metadata: {
status: 'approved',
collaborationType: request.collaborationType ?? null,
},
});
return {
approved: true,
request: await this.findVisibleRequestOrFail(targetUserId, request.id),
};
}
async reject(targetUserId: string, requestId: string) {
const request = await this.updateStatus(targetUserId, requestId, 'rejected');
return { rejected: true, request };
await this.createCollaborationNotification({
actorId: targetUserId,
recipientId: request.requesterId.toString(),
type: 'collaboration_request_rejected',
postId: request.postId.toString(),
requestId: request.id,
metadata: {
status: 'rejected',
collaborationType: request.collaborationType ?? null,
},
});
return {
rejected: true,
request: await this.findVisibleRequestOrFail(targetUserId, request.id),
};
}
private buildCollaborationDetails(dto: CreateCollaborationRequestDto) {
@@ -157,25 +254,87 @@ export class CollaborationRequestsService {
private async updateStatus(
targetUserId: string,
requestId: string,
status: 'approved' | 'rejected',
status: Extract<CollaborationRequestStatus, 'approved' | 'rejected'>,
) {
if (!Types.ObjectId.isValid(requestId)) {
throw new BadRequestException('Invalid collaboration request id');
}
const request = await this.collaborationRequestModel
.findOneAndUpdate(
{
_id: new Types.ObjectId(requestId),
targetUserId: new Types.ObjectId(targetUserId),
status: 'pending',
},
{ status },
{ new: true },
)
.exec();
const request = await this.collaborationRequestModel.findById(requestId).exec();
if (!request) {
throw new NotFoundException('Collaboration request not found');
}
if (request.targetUserId.toString() !== targetUserId) {
throw new ForbiddenException('Only the target user can update this collaboration request');
}
if (request.status !== 'pending') {
throw new BadRequestException('Only pending collaboration requests can be updated');
}
request.status = status;
await request.save();
return request;
}
private populateRequestQuery(query: any) {
return query
.populate({ path: 'requesterId', select: this.userSelect })
.populate({ path: 'targetUserId', select: this.userSelect })
.populate({ path: 'postId' });
}
private async findVisibleRequestOrFail(currentUserId: string, requestId: string) {
if (!Types.ObjectId.isValid(requestId)) {
throw new BadRequestException('Invalid collaboration request id');
}
const request = await this.populateRequestQuery(
this.collaborationRequestModel.findOne({
_id: new Types.ObjectId(requestId),
$or: [
{ requesterId: new Types.ObjectId(currentUserId) },
{ targetUserId: new Types.ObjectId(currentUserId) },
],
}),
).exec();
if (!request) {
throw new NotFoundException('Collaboration request not found');
}
return request;
}
private async createCollaborationNotification(options: {
actorId: string;
recipientId: string;
type:
| 'collaboration_request'
| 'collaboration_request_approved'
| 'collaboration_request_rejected'
| 'collaboration_request_cancelled';
postId: string;
requestId: string;
metadata?: Record<string, unknown>;
}) {
try {
await this.notificationsService.create({
actorId: options.actorId,
recipientId: options.recipientId,
type: options.type,
referenceId: options.postId,
resourceType: 'post',
deepLink: `/posts/${options.postId}`,
metadata: {
...(options.metadata ?? {}),
collaborationRequestId: options.requestId,
},
});
} catch (error) {
this.logger.warn(
`Collaboration notification failed: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
}