import { GrpcNotFoundException } from 'nestjs-grpc-exceptions'; import { Paginated, PaginateQuery } from 'nestjs-paginate'; import { IBaseMongoRepository } from '@/core/domain/interfaces/base.mongo.repository.interface'; import { instanceToPlain } from 'class-transformer'; import { ObjectId } from 'mongodb'; import { BaseMongoEntity } from '../infrastracture/databases/base.mongo.entity'; import { IBaseService } from './base.interface'; export class BaseMongoService< Entity, CreateMessage, UpdateMessage extends { id: string }, Message, > implements IBaseService { constructor(private readonly repository: IBaseMongoRepository) {} /** * Finds a single entity by its ID. * @param id - The ID of the entity to find (string or ObjectId). * @returns A promise that resolves to the mapped message. * @throws GrpcNotFoundException if the entity is not found. */ async findOne(id: string): Promise { const entity = (await this.repository.findById(id)) as BaseMongoEntity; const message = this.transformMongoEntityToMessage(entity); if (!entity) { throw new GrpcNotFoundException(`${this.constructor.name.replace('Service', '')} not found`); } return message; } /** * Retrieves all entities. * @returns A promise that resolves to an object containing an array of mapped messages. */ async findAll(): Promise<{ data: Message[] }> { const entities: Entity[] = await this.repository.find(); return { data: entities as unknown as Message[], }; } /** * Retrieves paginated entities based on the provided query. * @param query - Pagination and filter criteria. * @returns A promise that resolves to the paginated result with rows and count. */ async getMany(query?: PaginateQuery): Promise> { if (!query) return this.transformPaginatedMongoEntityToPaginatedMessage( (await this.repository.getMany({ path: '' })) as unknown as Paginated, ); const modifiedQuery: PaginateQuery = { ...query }; // // select → switch "id" to "_id" // if (Array.isArray(modifiedQuery.select)) { modifiedQuery.select = modifiedQuery.select.map(f => (f === 'id' ? '_id' : f)); } // // sortBy → array of arrays [[field, direction]] and switch "id" to "_id" // if (Array.isArray(modifiedQuery.sortBy)) { modifiedQuery.sortBy = modifiedQuery.sortBy.map(([field, dir]) => [field === 'id' ? '_id' : field, dir]); } // // filter → complex object { fieldName: value } and switch "id" to "_id" // if (modifiedQuery.filter && typeof modifiedQuery.filter === 'object') { const newFilter: Record = {}; for (const [key, val] of Object.entries(modifiedQuery.filter)) { const newKey = key.startsWith('id') ? key.replace(/^id\b/, '_id') : key === 'id' ? '_id' : key; newFilter[newKey] = val; } modifiedQuery.filter = newFilter; } // // cursorColumn switch "id" to "_id" // if (modifiedQuery.cursorColumn === 'id') { modifiedQuery.cursorColumn = '_id'; } const entities = (await this.repository.getMany(modifiedQuery)) as unknown as Paginated; // switch back _id to id for the response return this.transformPaginatedMongoEntityToPaginatedMessage(entities); } // simple switch id to _id protected transformMongoEntityToMessage(entity: BaseMongoEntity): Message { return { ...entity, id: entity._id.toString() } as unknown as Message; } // switch id to _id in the pagination object public transformPaginatedMongoEntityToPaginatedMessage( paginatedEntities: Paginated, ): Paginated { const transformedData = paginatedEntities.data.map(entity => { return this.transformMongoEntityToMessage(entity); }); const paginatedMessages: Paginated = { links: { ...paginatedEntities.links }, meta: { ...paginatedEntities.meta } as unknown as Paginated['meta'], data: transformedData, }; return paginatedMessages; } /** * Creates a new entity from the provided DTO. * @param createDTO - The data transfer object for creating an entity. * @returns A promise that resolves to the mapped message of the created entity. */ async create(createDTO: CreateMessage): Promise { const entity = this.repository.create(createDTO as unknown as Entity); for (const key of this.repository.paginateConfig.dateColumns) { entity[key] = createDTO[key] ? new Date(createDTO[key]) : null; } const savedEntity = await this.repository.save(entity); return this.transformMongoEntityToMessage(savedEntity as unknown as BaseMongoEntity); } /** * Updates an existing entity with the provided DTO. * @param updateDTO - The data transfer object for updating an entity. * @returns A promise that resolves to the mapped message of the updated entity. * @throws GrpcNotFoundException if the entity is not found. */ async update(updateDTO: UpdateMessage): Promise { const entity = await this.repository.findById(updateDTO.id); if (!entity) { throw new GrpcNotFoundException(`${this.constructor.name.replace('Service', '')} not found`); } const objectId = new ObjectId(updateDTO.id); await this.repository.update(objectId as any, instanceToPlain(updateDTO) as any); const updatedEntity: Entity = await this.repository.findById(updateDTO.id); return this.transformMongoEntityToMessage(updatedEntity as unknown as BaseMongoEntity); } /** * Deletes an entity by its ID. * @param id - The ID of the entity to delete (string). * @returns A promise that resolves when the entity is deleted. */ async remove(id: string): Promise { const objectId = new ObjectId(id); await this.repository.delete(objectId as any); } /** * Counts all entities. * @param options - Optional query options for counting entities. * @returns A promise that resolves to an object containing the count. */ async count(options?: any): Promise<{ count: number }> { const count = await this.repository.count(options || {}); return { count }; } }