diff --git a/api/src/nlp/controllers/nlp-value.controller.spec.ts b/api/src/nlp/controllers/nlp-value.controller.spec.ts index 14277994..deedf872 100644 --- a/api/src/nlp/controllers/nlp-value.controller.spec.ts +++ b/api/src/nlp/controllers/nlp-value.controller.spec.ts @@ -10,18 +10,14 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { getUpdateOneError } from '@/utils/test/errors/messages'; -import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity'; import { installNlpValueFixtures, nlpValueFixtures, } from '@/utils/test/fixtures/nlpvalue'; -import { getPageQuery } from '@/utils/test/pagination'; import { closeInMongodConnection, rootMongooseTestModule, } from '@/utils/test/test'; -import { TFixtures } from '@/utils/test/types'; -import { buildTestingMocks } from '@/utils/test/utils'; import { NlpValueCreateDto } from '../dto/nlp-value.dto'; import { NlpEntityRepository } from '../repositories/nlp-entity.repository'; @@ -29,11 +25,7 @@ import { NlpSampleEntityRepository } from '../repositories/nlp-sample-entity.rep import { NlpValueRepository } from '../repositories/nlp-value.repository'; import { NlpEntityModel } from '../schemas/nlp-entity.schema'; import { NlpSampleEntityModel } from '../schemas/nlp-sample-entity.schema'; -import { - NlpValue, - NlpValueFull, - NlpValueModel, -} from '../schemas/nlp-value.schema'; +import { NlpValue, NlpValueModel } from '../schemas/nlp-value.schema'; import { NlpEntityService } from '../services/nlp-entity.service'; import { NlpValueService } from '../services/nlp-value.service'; @@ -80,63 +72,6 @@ describe('NlpValueController', () => { afterEach(jest.clearAllMocks); - describe('findPage', () => { - it('should find nlp Values, and foreach nlp value populate the corresponding entity', async () => { - const pageQuery = getPageQuery({ - sort: ['value', 'desc'], - }); - const result = await nlpValueController.findPage( - pageQuery, - ['entity'], - {}, - ); - - const nlpValueFixturesWithEntities = nlpValueFixtures.reduce( - (acc, curr) => { - acc.push({ - ...curr, - entity: nlpEntityFixtures[ - parseInt(curr.entity!) - ] as NlpValueFull['entity'], - builtin: curr.builtin!, - expressions: curr.expressions!, - metadata: curr.metadata!, - }); - return acc; - }, - [] as TFixtures[], - ); - expect(result).toEqualPayload(nlpValueFixturesWithEntities); - }); - - it('should find nlp Values', async () => { - const pageQuery = getPageQuery({ - sort: ['value', 'desc'], - }); - const result = await nlpValueController.findPage( - pageQuery, - ['invalidCriteria'], - {}, - ); - const nlpEntities = await nlpEntityService.findAll(); - const nlpValueFixturesWithEntities = nlpValueFixtures.reduce( - (acc, curr) => { - const ValueWithEntities = { - ...curr, - entity: curr.entity ? nlpEntities[parseInt(curr.entity!)].id : null, - expressions: curr.expressions!, - metadata: curr.metadata!, - builtin: curr.builtin!, - }; - acc.push(ValueWithEntities); - return acc; - }, - [] as TFixtures[], - ); - expect(result).toEqualPayload(nlpValueFixturesWithEntities); - }); - }); - describe('count', () => { it('should count the nlp Values', async () => { const result = await nlpValueController.filterCount(); diff --git a/api/src/nlp/controllers/nlp-value.controller.ts b/api/src/nlp/controllers/nlp-value.controller.ts index 6c8c0e42..c7845f43 100644 --- a/api/src/nlp/controllers/nlp-value.controller.ts +++ b/api/src/nlp/controllers/nlp-value.controller.ts @@ -125,24 +125,8 @@ export class NlpValueController extends BaseController< return doc; } - @Get('') - async findAndPopulateWithCount( - @Query(PageQueryPipe) pageQuery: PageQueryDto, - @Query(PopulatePipe) populate: string[], - @Query( - new SearchFilterPipe({ allowedFields: ['entity', 'value'] }), - ) - filters: TFilterQuery, - ) { - return await this.nlpValueService.findAndPopulateWithCount( - pageQuery, - populate, - filters, - ); - } - /** - * Retrieves a paginated list of NLP values. + * Retrieves a paginated list of NLP values with NLP Samples count. * * Supports filtering, pagination, and optional population of related entities. * @@ -150,10 +134,10 @@ export class NlpValueController extends BaseController< * @param populate - An array of related entities to populate. * @param filters - Filters to apply when retrieving the NLP values. * - * @returns A promise resolving to a paginated list of NLP values. + * @returns A promise resolving to a paginated list of NLP values with NLP Samples count. */ - // @Get('') disabled - async findPage( + @Get() + async findWithCount( @Query(PageQueryPipe) pageQuery: PageQueryDto, @Query(PopulatePipe) populate: string[], @Query( @@ -164,8 +148,8 @@ export class NlpValueController extends BaseController< filters: TFilterQuery, ) { return this.canPopulate(populate) - ? await this.nlpValueService.findAndPopulate(filters, pageQuery) - : await this.nlpValueService.find(filters, pageQuery); + ? await this.nlpValueService.findAndPopulateWithCount(pageQuery, filters) + : await this.nlpValueService.findWithCount(pageQuery, filters); } /** diff --git a/api/src/nlp/repositories/nlp-value.repository.ts b/api/src/nlp/repositories/nlp-value.repository.ts index 54869623..b6a5eec8 100644 --- a/api/src/nlp/repositories/nlp-value.repository.ts +++ b/api/src/nlp/repositories/nlp-value.repository.ts @@ -8,21 +8,27 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Document, Model, Query, Types } from 'mongoose'; +import { plainToClass } from 'class-transformer'; +import { Document, Model, PipelineStage, Query, Types } from 'mongoose'; import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository'; import { PageQueryDto } from '@/utils/pagination/pagination-query.dto'; import { TFilterQuery } from '@/utils/types/filter.types'; import { NlpValueDto } from '../dto/nlp-value.dto'; +import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NLP_VALUE_POPULATE, NlpValue, NlpValueDocument, NlpValueFull, + NlpValueFullWithCount, NlpValuePopulate, + NlpValueWithCount, + TNlpValueCountFormat, } from '../schemas/nlp-value.schema'; +import { NlpEntityRepository } from './nlp-entity.repository'; import { NlpSampleEntityRepository } from './nlp-sample-entity.repository'; @Injectable() @@ -35,6 +41,8 @@ export class NlpValueRepository extends BaseRepository< constructor( @InjectModel(NlpValue.name) readonly model: Model, private readonly nlpSampleEntityRepository: NlpSampleEntityRepository, + @Inject(forwardRef(() => NlpEntityRepository)) + private readonly nlpEntityRepository: NlpEntityRepository, ) { super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull); } @@ -108,97 +116,162 @@ export class NlpValueRepository extends BaseRepository< } } - async findAndPopulateWithCount( + private async aggregateWithCount( { limit = 10, skip = 0, sort = ['createdAt', -1] }: PageQueryDto, - populate: string[], { $and = [], ...rest }: TFilterQuery, + populatePipelineStages: PipelineStage[] = [], ) { - return this.model - .aggregate([ - { - // support filters - $match: { - ...rest, - ...($and.length && { - $and: - $and.map(({ entity, ...rest }) => - entity - ? { - ...rest, - entity: new Types.ObjectId(String(entity)), - } - : rest, - ) || [], - }), + const pipeline: PipelineStage[] = [ + // support pageQuery + { + $limit: limit, + }, + { + $skip: skip, + }, + { + $sort: { + [sort[0]]: sort[1] === 'desc' ? -1 : 1, + _id: sort[1] === 'desc' ? -1 : 1, + }, + }, + { + // support filters + $match: { + ...rest, + ...($and.length && { + $and: + $and.map(({ entity, ...rest }) => + entity + ? { + ...rest, + entity: new Types.ObjectId(String(entity)), + } + : rest, + ) || [], + }), + }, + }, + { + $lookup: { + from: 'nlpsampleentities', + localField: '_id', + foreignField: 'value', + as: 'sampleEntities', + }, + }, + { + $unwind: { + path: '$sampleEntities', + preserveNullAndEmptyArrays: true, + }, + }, + { + $group: { + _id: '$_id', + value: { $first: '$value' }, + expressions: { $first: '$expressions' }, + builtin: { $first: '$builtin' }, + metadata: { $first: '$metadata' }, + createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + entity: { $first: '$entity' }, + nlpSamplesCount: { + $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, }, }, - // support pageQuery - { - $limit: limit, - }, - { - $skip: skip, - }, - { - $sort: { - [sort[0]]: sort[1] === 'desc' ? -1 : 1, - }, - }, - { - $lookup: { - from: 'nlpsampleentities', - localField: '_id', - foreignField: 'value', - as: 'sampleEntities', - }, - }, - { - $unwind: { - path: '$sampleEntities', - preserveNullAndEmptyArrays: true, - }, + }, + { + $project: { + id: '$_id', + _id: 0, + value: 1, + expressions: 1, + builtin: 1, + entity: 1, + metadata: 1, + createdAt: 1, + updatedAt: 1, + nlpSamplesCount: 1, }, + }, + ...populatePipelineStages, + ]; + + return await this.model.aggregate>(pipeline).exec(); + } + + private async plainToClass( + format: 'full' | 'stub', + aggregatedResults: (NlpValueWithCount | NlpValueFullWithCount)[], + ): Promise[]> { + if (format === 'full') { + const nestedNlpEntities: NlpValueFullWithCount[] = []; + for (const { entity, ...rest } of aggregatedResults) { + const plainNlpValue = { + ...rest, + entity: plainToClass( + NlpEntity, + await this.nlpEntityRepository.findOne(entity), + { + excludePrefixes: ['_'], + }, + ), + }; + nestedNlpEntities.push( + plainToClass(NlpValueFullWithCount, plainNlpValue, { + excludePrefixes: ['_'], + }), + ); + } + return nestedNlpEntities as TNlpValueCountFormat[]; + } else { + const nestedNlpEntities: NlpValueWithCount[] = []; + for (const aggregatedResult of aggregatedResults) { + nestedNlpEntities.push( + plainToClass(NlpValueWithCount, aggregatedResult, { + excludePrefixes: ['_'], + }), + ); + } + return nestedNlpEntities as TNlpValueCountFormat[]; + } + } + + async findWithCount( + pageQuery: PageQueryDto, + filterQuery: TFilterQuery, + ): Promise { + const aggregatedResults = await this.aggregateWithCount<'stub'>( + pageQuery, + filterQuery, + ); + + return await this.plainToClass<'stub'>('stub', aggregatedResults); + } + + async findAndPopulateWithCount( + pageQuery: PageQueryDto, + filterQuery: TFilterQuery, + ): Promise { + const aggregatedResults = await this.aggregateWithCount<'full'>( + pageQuery, + filterQuery, + [ { $lookup: { from: 'nlpentities', localField: 'entity', foreignField: '_id', - as: 'entities', + as: 'entity', }, }, { - $group: { - _id: '$_id', - value: { $first: '$value' }, - expressions: { $first: '$expressions' }, - builtin: { $first: '$builtin' }, - metadata: { $first: '$metadata' }, - createdAt: { $first: '$createdAt' }, - updatedAt: { $first: '$updatedAt' }, - entity: { - // support populate - $first: this.canPopulate(populate) ? '$entities' : '$entity', - }, - nlpSamplesCount: { - $sum: { $cond: [{ $ifNull: ['$sampleEntities', false] }, 1, 0] }, - }, - }, + $unwind: '$entity', }, - { - $project: { - id: '$_id', - _id: 0, - value: 1, - expressions: 1, - builtin: 1, - entity: 1, - metadata: 1, - createdAt: 1, - updatedAt: 1, - nlpSamplesCount: 1, - }, - }, - ]) - .exec(); + ], + ); + + return await this.plainToClass<'full'>('full', aggregatedResults); } } diff --git a/api/src/nlp/schemas/nlp-value.schema.ts b/api/src/nlp/schemas/nlp-value.schema.ts index 523eaa3e..77edcae1 100644 --- a/api/src/nlp/schemas/nlp-value.schema.ts +++ b/api/src/nlp/schemas/nlp-value.schema.ts @@ -106,6 +106,18 @@ export class NlpValueFull extends NlpValueStub { entity: NlpEntity; } +export class NlpValueWithCount extends NlpValue { + nlpSamplesCount: number; +} + +export class NlpValueFullWithCount extends NlpValueFull { + nlpSamplesCount: number; +} + +export class NlpValueFullWithCountDto { + nlpSamplesCount: number; +} + export type NlpValueDocument = THydratedDocument; export const NlpValueModel: ModelDefinition = LifecycleHookManager.attach({ @@ -121,3 +133,7 @@ export type NlpValuePopulate = keyof TFilterPopulateFields< >; export const NLP_VALUE_POPULATE: NlpValuePopulate[] = ['entity']; + +export type TNlpValueCountFormat = T extends 'stub' + ? NlpValueWithCount + : NlpValueFullWithCount; diff --git a/api/src/nlp/services/nlp-value.service.ts b/api/src/nlp/services/nlp-value.service.ts index 6e5ccc35..23448440 100644 --- a/api/src/nlp/services/nlp-value.service.ts +++ b/api/src/nlp/services/nlp-value.service.ts @@ -19,7 +19,9 @@ import { NlpEntity } from '../schemas/nlp-entity.schema'; import { NlpValue, NlpValueFull, + NlpValueFullWithCount, NlpValuePopulate, + NlpValueWithCount, } from '../schemas/nlp-value.schema'; import { NlpSampleEntityValue } from '../schemas/types'; @@ -221,15 +223,17 @@ export class NlpValueService extends BaseService< return Promise.all(promises); } + async findWithCount( + pageQuery: PageQueryDto, + filters: TFilterQuery, + ): Promise { + return await this.repository.findWithCount(pageQuery, filters); + } + async findAndPopulateWithCount( pageQuery: PageQueryDto, - populate: string[], filters: TFilterQuery, - ) { - return await this.repository.findAndPopulateWithCount( - pageQuery, - populate, - filters, - ); + ): Promise { + return await this.repository.findAndPopulateWithCount(pageQuery, filters); } }