feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s

هذا الالتزام موجود في:
2026-05-14 16:17:12 +03:00
الأصل 0e76a4a9fc
التزام 5bd5e19a89
158 ملفات معدلة مع 19563 إضافات و3315 حذوفات

عرض الملف

@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
import { NotificationsModule } from '../notifications/notifications.module';
import { UsersModule } from '../users/users.module';
import { ChatController } from './chat.controller';
import { ChatGateway } from './chat.gateway';
@@ -15,6 +16,7 @@ import { Message, MessageSchema } from './schemas/message.schema';
imports: [
ConfigModule,
JwtModule.register({}),
NotificationsModule,
UsersModule,
MongooseModule.forFeature([
{ name: Conversation.name, schema: ConversationSchema },

عرض الملف

@@ -51,11 +51,16 @@ export class ChatRepository {
});
}
async findConversationsForUser(userId: string, skip: number, limit: number): Promise<ConversationDocument[]> {
async findConversationsForUser(
userId: string,
skip: number,
limit: number,
sort: Record<string, 1 | -1> = { lastMessageAt: -1, updatedAt: -1 },
): Promise<ConversationDocument[]> {
return this.conversationModel
.find({ participantIds: new Types.ObjectId(userId) })
.populate({ path: 'participantIds', select: 'name username stageName avatar isVerified isDisabled' })
.sort({ lastMessageAt: -1, updatedAt: -1 })
.sort(sort)
.skip(skip)
.limit(limit)
.exec();
@@ -83,11 +88,16 @@ export class ChatRepository {
});
}
async findMessages(conversationId: string, skip: number, limit: number): Promise<MessageDocument[]> {
async findMessages(
conversationId: string,
skip: number,
limit: number,
sort: Record<string, 1 | -1> = { createdAt: -1 },
): Promise<MessageDocument[]> {
return this.messageModel
.find({ conversationId: new Types.ObjectId(conversationId) })
.populate({ path: 'senderId', select: 'name username stageName avatar isVerified' })
.sort({ createdAt: -1 })
.sort(sort)
.skip(skip)
.limit(limit)
.exec();

عرض الملف

@@ -1,6 +1,9 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { decodeOffsetCursor, encodeOffsetCursor } from '../../common/utils/cursor.util';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { NotificationsService } from '../notifications/notifications.service';
import { UsersRepository } from '../users/users.repository';
import { CreateConversationDto } from './dto/create-conversation.dto';
import { MessageQueryDto } from './dto/message-query.dto';
@@ -9,9 +12,12 @@ import { ChatRepository } from './chat.repository';
@Injectable()
export class ChatService {
private readonly logger = new Logger(ChatService.name);
constructor(
private readonly chatRepository: ChatRepository,
private readonly usersRepository: UsersRepository,
private readonly notificationsService: NotificationsService,
) {}
async createConversation(currentUserId: string, dto: CreateConversationDto) {
@@ -61,9 +67,13 @@ export class ChatService {
const limit = query.limit ?? 20;
const cursorOffset = decodeOffsetCursor(query.cursor);
const skip = cursorOffset ?? (page - 1) * limit;
const direction = resolveMongoSortDirection(query.sortOrder);
const [items, total] = await Promise.all([
this.chatRepository.findConversationsForUser(currentUserId, skip, limit),
this.chatRepository.findConversationsForUser(currentUserId, skip, limit, {
lastMessageAt: direction,
updatedAt: direction,
}),
this.chatRepository.countConversationsForUser(currentUserId),
]);
@@ -78,14 +88,15 @@ export class ChatService {
const nextOffset = skip + mappedItems.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
return {
items: mappedItems,
return buildPaginatedResponse(mappedItems, {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
offset: skip,
currentCursor: query.cursor ?? null,
nextCursor,
};
mode: 'cursor',
});
}
async getMessages(currentUserId: string, conversationId: string, query: MessageQueryDto) {
@@ -94,9 +105,10 @@ export class ChatService {
const limit = query.limit ?? 20;
const cursorOffset = decodeOffsetCursor(query.cursor);
const skip = cursorOffset ?? (page - 1) * limit;
const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
const [items, total] = await Promise.all([
this.chatRepository.findMessages(conversation.id, skip, limit),
this.chatRepository.findMessages(conversation.id, skip, limit, sort),
this.chatRepository.countMessages(conversation.id),
]);
@@ -104,14 +116,15 @@ export class ChatService {
const nextOffset = skip + items.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
return {
items,
return buildPaginatedResponse(items, {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
offset: skip,
currentCursor: query.cursor ?? null,
nextCursor,
};
mode: 'cursor',
});
}
async sendMessage(currentUserId: string, dto: SendMessageDto) {
@@ -144,6 +157,12 @@ export class ChatService {
currentUserId,
preview,
);
await this.dispatchMessageNotifications(
currentUserId,
conversation.participantIds.map((id) => id.toString()),
conversation.id,
preview,
);
return message;
}
@@ -247,4 +266,32 @@ export class ChatService {
}
}
}
private async dispatchMessageNotifications(
actorId: string,
participantIds: string[],
conversationId: string,
previewText: string,
): Promise<void> {
for (const recipientId of participantIds) {
if (recipientId === actorId) {
continue;
}
try {
await this.notificationsService.createMessageNotification(
actorId,
recipientId,
conversationId,
previewText.slice(0, 160),
);
} catch (error) {
this.logger.warn(
`Message notification failed for actor=${actorId} recipient=${recipientId}: ${
error instanceof Error ? error.message : 'unknown error'
}`,
);
}
}
}
}

عرض الملف

@@ -1,5 +1,7 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsArray, IsBoolean, IsOptional, IsString, Length } from 'class-validator';
import { toBoolean } from '../../../common/utils/query-transform.util';
export class CreateConversationDto {
@IsArray()
@@ -8,6 +10,7 @@ export class CreateConversationDto {
@ApiPropertyOptional({ default: false })
@IsOptional()
@Transform(toBoolean)
@IsBoolean()
isGroup?: boolean;

عرض الملف

@@ -1,5 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
import { resolveManagedFileUrl } from '../../../common/utils/public-url.util';
import { User } from '../../users/schemas/user.schema';
export type MessageDocument = HydratedDocument<Message>;
@@ -31,3 +32,11 @@ export class Message {
export const MessageSchema = SchemaFactory.createForClass(Message);
MessageSchema.index({ conversationId: 1, createdAt: -1 });
MessageSchema.index({ conversationId: 1, isUnsent: 1, createdAt: -1 });
const transformManagedMessageFiles = (_doc: unknown, ret: any) => {
ret.mediaUrl = resolveManagedFileUrl(ret.mediaUrl);
return ret;
};
MessageSchema.set('toJSON', { transform: transformManagedMessageFiles });
MessageSchema.set('toObject', { transform: transformManagedMessageFiles });