Improve collaboration requests APIs
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
@@ -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)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم