diff --git a/src/domain-services/categories/category-service.ts b/src/domain-services/categories/category-service.ts index c7fe5131..b356f4e6 100644 --- a/src/domain-services/categories/category-service.ts +++ b/src/domain-services/categories/category-service.ts @@ -1,34 +1,26 @@ import { type Database } from '@unocha/hpc-api-core/src/db'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; import { type InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; import { Service } from 'typedi'; import { type Category } from './graphql/types'; @Service() export class CategoryService { async getCategoriesForFlows( - flowLinks: Map>>, + flowIDs: FlowId[], models: Database ): Promise> { - const flowLinksBrandedIds = []; - for (const flowLink of flowLinks.keys()) { - flowLinksBrandedIds.push(createBrandedValue(flowLink)); - } - // Group categories by flow ID for easy mapping const categoriesMap = new Map(); - if (flowLinksBrandedIds.length === 0) { - return categoriesMap; - } - const categoriesRef: Array> = await models.categoryRef.find({ where: { objectID: { - [Op.IN]: flowLinksBrandedIds, + [Op.IN]: flowIDs, }, + objectType: 'flow', }, }); @@ -89,4 +81,20 @@ export class CategoryService { }, }; } + + async findCategories(models: Database, where: any) { + const category = await models.category.find({ + where, + }); + + return category; + } + + async findCategoryRefs(models: Database, where: any) { + const categoryRef = await models.categoryRef.find({ + where, + }); + + return categoryRef; + } } diff --git a/src/domain-services/categories/model.ts b/src/domain-services/categories/model.ts new file mode 100644 index 00000000..a96d8f62 --- /dev/null +++ b/src/domain-services/categories/model.ts @@ -0,0 +1,27 @@ +export type CategoryGroup = + | 'beneficiaryGroup' + | 'contributionStatus' + | 'contributionType' + | 'customLocation' + | 'earmarkingType' + | 'emergencyType' + | 'flowStatus' + | 'flowType' + | 'genderMarker' + | 'inactiveReason' + | 'keywords' + | 'method' + | 'organizationLevel' + | 'organizationType' + | 'pendingStatus' + | 'planCosting' + | 'planIndicated' + | 'planType' + | 'projectGrouping1' + | 'projectGrouping2' + | 'projectPriority' + | 'regions' + | 'reportChannel' + | 'responseType' + | 'sectorIASC' + | 'subsetOfPlan'; diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index ca0ee5db..493e4f45 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -18,6 +18,7 @@ import { ReportDetailService } from '../report-details/report-detail-service'; import { type UsageYear } from '../usage-years/grpahql/types'; import { UsageYearService } from '../usage-years/usage-year-service'; import { + FlowCategoryFilters, type FlowObjectFilters, type SearchFlowsArgs, type SearchFlowsArgsNonPaginated, @@ -33,8 +34,8 @@ import { } from './graphql/types'; import { type FlowEntity } from './model'; import { type FlowSearchStrategy } from './strategy/flow-search-strategy'; -import { FlowObjectFiltersStrategy } from './strategy/impl/flow-object-conditions-strategy'; -import { OnlyFlowFiltersStrategy } from './strategy/impl/only-flow-conditions-strategy'; +import { FlowObjectFiltersStrategy } from './strategy/impl/flow-object-conditions-strategy-impl'; +import { OnlyFlowFiltersStrategy } from './strategy/impl/only-flow-conditions-strategy-impl'; @Service() export class FlowSearchService { @@ -56,7 +57,8 @@ export class FlowSearchService { models: Database, filters: SearchFlowsArgs ): Promise { - const { limit, afterCursor, beforeCursor, sortField, sortOrder } = filters; + const { limit, nextPageCursor, prevPageCursor, sortField, sortOrder } = + filters; const orderBy: | { column: FlowSortField; order: 'asc' | 'desc' } @@ -65,18 +67,19 @@ export class FlowSearchService { order: sortOrder ?? 'desc', }; - const { flowFilters, flowObjectFilters } = filters; + const { flowFilters, flowObjectFilters, flowCategoryFilters } = filters; const cursorCondition = this.buildCursorCondition( - beforeCursor, - afterCursor, + prevPageCursor, + nextPageCursor, orderBy ); // Determine strategy of how to search for flows const { strategy, conditions } = this.determineStrategy( flowFilters, - flowObjectFilters + flowObjectFilters, + flowCategoryFilters ); // Fetch one more item to check for hasNextPage @@ -137,7 +140,7 @@ export class FlowSearchService { usageYearsMap, reportDetailsMap, ] = await Promise.all([ - this.categoryService.getCategoriesForFlows(flowLinksMap, models), + this.categoryService.getCategoriesForFlows(flowIds, models), this.organizationService.getOrganizationsForFlows( organizationsFO, models @@ -210,9 +213,9 @@ export class FlowSearchService { return { flows: items, hasNextPage: limit <= flows.length, - hasPreviousPage: afterCursor !== undefined, - startCursor: flows.length ? items[0].cursor : '', - endCursor: flows.length ? items.at(-1)?.cursor ?? '' : '', + hasPreviousPage: nextPageCursor !== undefined, + prevPageCursor: flows.length ? items[0].cursor : '', + nextPageCursor: flows.length ? items.at(-1)?.cursor ?? '' : '', pageSize: flows.length, sortField: sortField ?? 'updatedAt', sortOrder: sortOrder ?? 'desc', @@ -274,59 +277,93 @@ export class FlowSearchService { determineStrategy( flowFilters: SearchFlowsFilters, - flowObjectFilters: FlowObjectFilters[] + flowObjectFilters: FlowObjectFilters[], + flowCategoryFilters: FlowCategoryFilters ): { strategy: FlowSearchStrategy; conditions: any } { - let conditions = {}; - const isFlowFilterDefined = flowFilters !== undefined; const isFlowObjectFilterDefined = flowObjectFilters !== undefined; const isFlowObjectFiltersNotEmpty = isFlowObjectFilterDefined && flowObjectFilters.length !== 0; + const isFlowCategoryFilterDefined = flowCategoryFilters !== undefined; + if ( (!isFlowFilterDefined && - (!isFlowObjectFilterDefined || !isFlowObjectFiltersNotEmpty)) || + (!isFlowObjectFilterDefined || !isFlowObjectFiltersNotEmpty) && + !isFlowCategoryFilterDefined) || (isFlowFilterDefined && - (!isFlowObjectFilterDefined || !isFlowObjectFiltersNotEmpty)) + (!isFlowObjectFilterDefined || !isFlowObjectFiltersNotEmpty) && + !isFlowCategoryFilterDefined) ) { const flowConditions = this.prepareFlowConditions(flowFilters); - conditions = { ...conditions, ...flowConditions }; - return { strategy: this.onlyFlowFiltersStrategy, conditions }; - } else if (!isFlowFilterDefined && isFlowObjectFiltersNotEmpty) { + // conditions = { ...conditions, ...flowConditions }; + return { + strategy: this.onlyFlowFiltersStrategy, + conditions: flowConditions, + }; + } else if ( + !isFlowFilterDefined && + isFlowObjectFiltersNotEmpty && + !isFlowCategoryFilterDefined + ) { const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); - conditions = { ...conditions, ...flowObjectConditions }; + // conditions = { ...conditions, ...flowObjectConditions }; return { strategy: this.flowObjectFiltersStrategy, - conditions: this.buildConditionsMap(undefined, flowObjectConditions), + conditions: { + conditionsMap: this.buildConditionsMap([flowObjectConditions]), + flowCategoryFilters: {}, + }, + }; + } else if ( + !isFlowFilterDefined && + !isFlowObjectFiltersNotEmpty && + isFlowCategoryFilterDefined + ) { + return { + strategy: this.flowObjectFiltersStrategy, + conditions: { conditionsMap: new Map(), flowCategoryFilters }, }; } else if (isFlowFilterDefined && isFlowObjectFiltersNotEmpty) { const flowConditions = this.prepareFlowConditions(flowFilters); const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); - conditions = { - ...conditions, - ...flowConditions, - ...flowObjectConditions, - }; + // conditions = { + // ...conditions, + // ...flowConditions, + // ...flowObjectConditions, + // }; return { strategy: this.flowObjectFiltersStrategy, - conditions: this.buildConditionsMap( - flowConditions, - flowObjectConditions - ), + conditions: { + conditionsMap: this.buildConditionsMap([ + flowConditions, + flowObjectConditions, + ]), + flowCategoryFilters, + }, }; } throw new Error('Invalid combination of flowFilters and flowObjectFilters'); } - private buildConditionsMap(flowConditions: any, flowObjectConditions: any) { + private buildConditionsMap(conditions: any[]) { const conditionsMap = new Map(); - conditionsMap.set('flowObjects', flowObjectConditions); - conditionsMap.set('flow', flowConditions); + conditions.forEach((condition) => { + for (const [key, value] of Object.entries(condition)) { + if (value && Array.isArray(value) && value.length === 0) { + const nestedConditions = this.buildConditionsMap([value]); + conditionsMap.set(key, nestedConditions); + } else if (value) { + conditionsMap.set(key, value); + } + } + }); + return conditionsMap; } @@ -510,11 +547,12 @@ export class FlowSearchService { models: Database, args: SearchFlowsArgsNonPaginated ): Promise { - const { flowFilters, flowObjectFilters } = args; + const { flowFilters, flowObjectFilters, flowCategoryFilters } = args; const { strategy, conditions } = this.determineStrategy( flowFilters, - flowObjectFilters + flowObjectFilters, + flowCategoryFilters ); const { flows, count } = await strategy.search(conditions, models); @@ -544,8 +582,8 @@ export class FlowSearchService { let hasNextPage = flowSearchResponse.hasNextPage; - let cursor = flowSearchResponse.endCursor; - let nextArgs: SearchFlowsArgs = { ...args, afterCursor: cursor }; + let cursor = flowSearchResponse.nextPageCursor; + let nextArgs: SearchFlowsArgs = { ...args, nextPageCursor: cursor }; let nextFlowSearchResponse: FlowSearchResult; while (hasNextPage) { @@ -554,10 +592,10 @@ export class FlowSearchService { flows.push(...nextFlowSearchResponse.flows); hasNextPage = nextFlowSearchResponse.hasNextPage; - cursor = nextFlowSearchResponse.endCursor; + cursor = nextFlowSearchResponse.nextPageCursor; // Update the cursor for the next iteration - nextArgs = { ...args, afterCursor: cursor }; + nextArgs = { ...args, nextPageCursor: cursor }; } return { flows, flowsCount: flows.length }; diff --git a/src/domain-services/flows/graphql/args.ts b/src/domain-services/flows/graphql/args.ts index fab20a24..b58bfd69 100644 --- a/src/domain-services/flows/graphql/args.ts +++ b/src/domain-services/flows/graphql/args.ts @@ -29,6 +29,15 @@ export class SearchFlowsFilters { legacyId: number; } +@InputType() +export class FlowCategoryFilters { + @Field({ nullable: true }) + pending: boolean; + + @Field(() => [FlowCategory], { nullable: true }) + categoryFilters: FlowCategory[]; +} + @InputType() export class FlowObjectFilters { @Field({ nullable: false }) @@ -51,11 +60,14 @@ export class FlowObjectFilters { @InputType() export class FlowCategory { - @Field({ nullable: false }) + @Field({ nullable: true }) id: number; - @Field({ nullable: false }) + @Field({ nullable: true }) group: string; + + @Field({ nullable: true }) + name: string; } @ArgsType() @@ -66,11 +78,11 @@ export class SearchFlowsArgs extends PaginationArgs { @Field(() => [FlowObjectFilters], { nullable: true }) flowObjectFilters: FlowObjectFilters[]; - @Field(() => [FlowCategory], { nullable: true }) - categoryFilters: FlowCategory[]; - @Field({ nullable: true }) includeChildrenOfParkedFlows: boolean; + + @Field({ nullable: true }) + flowCategoryFilters: FlowCategoryFilters; } @ArgsType() @@ -81,9 +93,9 @@ export class SearchFlowsArgsNonPaginated { @Field(() => [FlowObjectFilters], { nullable: true }) flowObjectFilters: FlowObjectFilters[]; - @Field(() => [FlowCategory], { nullable: true }) - categoryFilters: FlowCategory[]; - @Field({ nullable: true }) includeChildrenOfParkedFlows: boolean; + + @Field({ nullable: true }) + flowCategoryFilters: FlowCategoryFilters; } diff --git a/src/domain-services/flows/strategy/flow-search-strategy.ts b/src/domain-services/flows/strategy/flow-search-strategy.ts index 2d2d4015..c2cb3a19 100644 --- a/src/domain-services/flows/strategy/flow-search-strategy.ts +++ b/src/domain-services/flows/strategy/flow-search-strategy.ts @@ -8,7 +8,9 @@ export interface FlowSearchStrategyResponse { export interface FlowSearchStrategy { search( - flowConditions: Map, + flowConditions: + | Map + | { conditionsMap: Map; flowCategoryFilters: any }, models: Database, orderBy?: any, limit?: number, diff --git a/src/domain-services/flows/strategy/flowID-search-strategy.ts b/src/domain-services/flows/strategy/flowID-search-strategy.ts new file mode 100644 index 00000000..fa481211 --- /dev/null +++ b/src/domain-services/flows/strategy/flowID-search-strategy.ts @@ -0,0 +1,16 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { FlowCategoryFilters } from '../graphql/args'; +import { type FlowEntity } from '../model'; + +export interface FlowIdSearchStrategyResponse { + flowIDs: FlowId[]; +} + +export interface FlowIDSearchStrategy { + search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise; +} diff --git a/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy-impl.ts new file mode 100644 index 00000000..108e6a68 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy-impl.ts @@ -0,0 +1,224 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { Cond, Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { Service } from 'typedi'; +import { FlowService } from '../../flow-service'; +import { FlowCategoryFilters } from '../../graphql/args'; +import { + type FlowSearchStrategy, + type FlowSearchStrategyResponse, +} from '../flow-search-strategy'; +import { + FlowIDSearchStrategy, + FlowIdSearchStrategyResponse, +} from '../flowID-search-strategy'; +import { GetFlowIdsFromCategoryConditionsStrategyImpl } from './get-flowIds-flow-category-conditions-strategy-impl'; +import { GetFlowIdsFromMixedConditionsStrategyImpl } from './get-flowIds-flow-mixed-conditions-strategy-impl'; + +@Service() +export class FlowObjectFiltersStrategy implements FlowSearchStrategy { + constructor( + private readonly flowService: FlowService, + private readonly getFlowIdsFromObjectConditions: GetFlowIdsFromMixedConditionsStrategyImpl, + private readonly getFlowIdsFromCategoryConditions: GetFlowIdsFromCategoryConditionsStrategyImpl, + private readonly getFlowIdsFromMixedConditions: GetFlowIdsFromMixedConditionsStrategyImpl + ) {} + + async search( + flowConditions: { + conditionsMap: Map; + flowCategoryFilters: FlowCategoryFilters; + }, + models: Database, + orderBy: any, + limit: number, + cursorCondition?: any + ): Promise { + const flowConditionsMap = flowConditions.conditionsMap; + // Obtain flowObjects conditions + const flowObjectsConditions: Map< + string, + Map + > = flowConditionsMap.get('flowObjects') ?? new Map(); + + // Obtain flow conditions + const flowEntityConditions = flowConditionsMap.get('flow') ?? new Map(); + + // Obtain flowCategory conditions + const flowCategoryConditions = flowConditions.flowCategoryFilters ?? {}; + + const searchFlowIdsStrategy: FlowIDSearchStrategy = this.determineStrategy( + flowObjectsConditions, + flowCategoryConditions + ); + + const { flowIDs: flowIdsToFilter }: FlowIdSearchStrategyResponse = + await searchFlowIdsStrategy.search( + models, + flowObjectsConditions, + flowCategoryConditions + ); + + // Due the possibility of having a large amount of flowIDs + // we need to split the query in batches + // then combine the results + const { flows, count } = await this.getFlowsAndCountInBatches( + models, + flowIdsToFilter, + orderBy, + limit, + flowEntityConditions, + cursorCondition + ); + + return { flows, count }; + } + + // Determine the strategy to use in order to obtain flowIDs + // aiming to have the least amount of flowIDs to filter + // in the next step + // If there are flowObjects conditions + // use flowObjects strategy + // otherwise use flowCategories strategy + // If there are both flowObjects and flowCategories conditions + // use both and merge the results keeping only flowIDs + // present in both arrays + // otherwise keep all flowIDs from the one that is not empty + determineStrategy( + flowObjectsConditions: Map>, + flowCategoryConditions: any + ): any { + const flowObjectsConditionsIsDefined = flowObjectsConditions !== undefined; + const flowCategoryConditionsIsDefined = + flowCategoryConditions !== undefined; + + const flowObjectsConditionsIsNotEmpty = + flowObjectsConditionsIsDefined && flowObjectsConditions.size; + const flowCategoryConditionsIsNotEmpty = + flowCategoryConditionsIsDefined && + Object.keys(flowCategoryConditions).length; + + if (flowObjectsConditionsIsNotEmpty && flowCategoryConditionsIsNotEmpty) { + return this.getFlowIdsFromMixedConditions; + } else if (flowObjectsConditionsIsNotEmpty) { + return this.getFlowIdsFromObjectConditions; + } else if (flowCategoryConditionsIsNotEmpty) { + return this.getFlowIdsFromCategoryConditions; + } + throw new Error( + 'No strategy found for flowObjectsConditions and flowCategoryConditions' + ); + } + + getIdConditionsFromFlowIDs( + flowIDsFromFilteredFlowObjectsAndFlowCategories: FlowId[] + ) { + if ( + !flowIDsFromFilteredFlowObjectsAndFlowCategories || + !flowIDsFromFilteredFlowObjectsAndFlowCategories.length + ) { + return {}; + } + + return { + id: { + [Op.IN]: flowIDsFromFilteredFlowObjectsAndFlowCategories, + }, + }; + } + + async getFlowsAndCountInBatches( + models: Database, + flowIDs: FlowId[], + orderBy: any, + limit: number, + flowEntityConditions?: any, + cursorCondition?: any + ): Promise<{ flows: any[]; count: number }> { + const flowIDsBatches = this.splitFlowIDsInBatches(flowIDs); + + const countResponses = []; + const flowsResponses = []; + for (let i = 0; i < flowIDsBatches.length; i++) { + const flowIDsBatch = flowIDsBatches[i]; + const prevFlowIDsBatch = i === 0 ? [] : flowIDsBatches[i - 1]; + // Combine conditions from flowObjects FlowIDs and flow conditions + const countConditions = { + [Cond.AND]: [ + flowEntityConditions ?? {}, + + { + id: { + [Op.IN]: flowIDsBatch, + [Op.NOT_IN]: prevFlowIDsBatch, + }, + }, + ], + }; + + // Combine cursor condition with flow conditions + const searchConditions = { + [Cond.AND]: [ + flowEntityConditions ?? {}, + cursorCondition ?? {}, + { + id: { + [Op.IN]: flowIDsBatch, + }, + }, + ], + }; + + const flowsMissing = limit - flowsResponses.length; + // If we have enough flows, stop querying flows + // but keep querying counts + if (flowsMissing <= 0) { + const countPromise = this.flowService.getFlowsCount( + models, + countConditions + ); + const countRes = await countPromise; + const count = countRes[0] as { count: number }; + countResponses.push(count.count); + continue; + } + + const flowsPromise = this.flowService.getFlows( + models, + searchConditions, + orderBy, + limit + ); + + const countPromise = this.flowService.getFlowsCount( + models, + countConditions + ); + + const [flows, countsRes] = await Promise.all([ + flowsPromise, + countPromise, + ]); + + const count = countsRes[0] as { count: number }; + countResponses.push(count.count); + + const flowsToAdd = flows.slice(0, flowsMissing); + flowsResponses.push(...flowsToAdd); + } + + return { + flows: flowsResponses, + count: countResponses.reduce((a, b) => a + b, 0).valueOf(), + }; + } + + splitFlowIDsInBatches(flowIDs: FlowId[]) { + const flowIDsBatches = []; + const batchSize = 10000; + for (let i = 0; i < flowIDs.length; i += batchSize) { + flowIDsBatches.push(flowIDs.slice(i, i + batchSize)); + } + return flowIDsBatches; + } +} diff --git a/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy.ts b/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy.ts deleted file mode 100644 index 51d16337..00000000 --- a/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { type Database } from '@unocha/hpc-api-core/src/db'; -import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; -import { Cond, Op } from '@unocha/hpc-api-core/src/db/util/conditions'; -import { Service } from 'typedi'; -import { FlowObjectService } from '../../../flow-object/flow-object-service'; -import { FlowService } from '../../flow-service'; -import { - type FlowSearchStrategy, - type FlowSearchStrategyResponse, -} from '../flow-search-strategy'; - -@Service() -export class FlowObjectFiltersStrategy implements FlowSearchStrategy { - constructor( - private readonly flowService: FlowService, - private readonly flowObjectService: FlowObjectService - ) {} - - async search( - flowConditions: Map, - models: Database, - orderBy?: any, - limit?: number, - cursorCondition?: any - ): Promise { - // Obtain flowObjects conditions - const flowObjectsConditions: Map< - string, - Map - > = flowConditions.get('flowObjects') ?? new Map(); - - // Obtain flow conditions - const flowEntityConditions = flowConditions.get('flow') ?? new Map(); - - // Obtain where clause for flowObjects - const flowObjectWhere = this.mapFlowObjectConditionsToWhereClause( - flowObjectsConditions - ); - - // Obtain flowIDs based on provided flowObject conditions - const flowIDsFromFilteredFlowObjects: FlowId[] = - await this.getFlowIDsFromFilteredFlowObjects(models, flowObjectWhere); - - // Combine conditions from flowObjects FlowIDs and flow conditions - const countConditions = { - [Cond.AND]: [ - flowEntityConditions ?? {}, - { - id: { - [Op.IN]: flowIDsFromFilteredFlowObjects, - }, - }, - ], - }; - - // Combine cursor condition with flow conditions - const searchConditions = { - [Cond.AND]: [ - flowEntityConditions ?? {}, - cursorCondition ?? {}, - { - id: { - [Op.IN]: flowIDsFromFilteredFlowObjects, - }, - }, - ], - }; - - // Obtain flows and flowCount based on flowIDs from filtered flowObjects - // and flow conditions - const [flows, countRes] = await Promise.all([ - this.flowService.getFlows(models, searchConditions, orderBy, limit), - this.flowService.getFlowsCount(models, countConditions), - ]); - - // Map count result query to count object - const countObject = countRes[0] as { count: number }; - - return { flows, count: countObject.count }; - } - - private async getFlowIDsFromFilteredFlowObjects( - models: Database, - flowObjectWhere: any[] - ): Promise { - const flowIDsFromFilteredFlowObjects: FlowId[] = []; - const tempFlowIDs: FlowId[][] = await Promise.all( - flowObjectWhere.map((whereClause) => - this.flowObjectService.getFlowIdsFromFlowObjects(models, whereClause) - ) - ); - // Flatten array of arrays keeping only values present in all arrays - const flowIDs = tempFlowIDs.reduce((a, b) => - a.filter((c) => b.includes(c)) - ); - flowIDsFromFilteredFlowObjects.push(...flowIDs); - - return flowIDsFromFilteredFlowObjects.sort(); - } - - /* - * Map structure: - * { - * KEY = objectType: string, - * VALUE = { - * KEY = refDirection: string, - * VALUE = [objectID: number] - * } - * } - */ - private mapFlowObjectConditionsToWhereClause( - flowObjectConditions: Map> - ): any[] { - const whereClauses: any = []; - for (const [objectType, refDirectionMap] of flowObjectConditions) { - for (const [refDirection, objectIDs] of refDirectionMap) { - const whereClause = { - objectID: { - [Op.IN]: objectIDs, - }, - refDirection: { - [Op.LIKE]: refDirection, - }, - objectType: { - [Op.LIKE]: objectType, - }, - }; - - whereClauses.push(whereClause); - } - } - - return whereClauses; - } -} diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts new file mode 100644 index 00000000..17801615 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts @@ -0,0 +1,56 @@ +import { Database } from '@unocha/hpc-api-core/src/db'; +import { CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; +import { Service } from 'typedi'; +import { CategoryService } from '../../../categories/category-service'; +import { FlowCategoryFilters } from '../../graphql/args'; +import { + FlowIDSearchStrategy, + FlowIdSearchStrategyResponse, +} from '../flowID-search-strategy'; +import { mapFlowCategoryConditionsToWhereClause } from './utils'; + +@Service() +export class GetFlowIdsFromCategoryConditionsStrategyImpl + implements FlowIDSearchStrategy +{ + constructor(private readonly categoryService: CategoryService) {} + + async search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise { + const whereClause = mapFlowCategoryConditionsToWhereClause( + flowCategoryConditions + ); + + const categories = await this.categoryService.findCategories( + models, + whereClause + ); + + const categoriesIds: CategoryId[] = categories.map( + (category) => category.id + ); + + const categoryRefs = await this.categoryService.findCategoryRefs(models, { + categoryID: { + [Op.IN]: categoriesIds, + }, + objectType: 'flow', + }); + + // Map category refs to flow IDs + // keep only unique values + // and return the list of flow IDs + const flowIds = Array.from( + new Set(categoryRefs.map((categoryRef) => categoryRef.objectID)) + ) + .map((flowId) => createBrandedValue(flowId)) + .sort(); + + return { flowIDs: flowIds }; + } +} diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-mixed-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-mixed-conditions-strategy-impl.ts new file mode 100644 index 00000000..6db16389 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-mixed-conditions-strategy-impl.ts @@ -0,0 +1,47 @@ +import { Database } from '@unocha/hpc-api-core/src/db'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { Service } from 'typedi'; +import { FlowCategoryFilters } from '../../graphql/args'; +import { + FlowIDSearchStrategy, + FlowIdSearchStrategyResponse, +} from '../flowID-search-strategy'; +import { mergeFlowIDsFromFilteredFlowObjectsAndFlowCategories } from './utils'; + +@Service() +export class GetFlowIdsFromMixedConditionsStrategyImpl + implements FlowIDSearchStrategy +{ + constructor( + private readonly getFlowIdsFromObjectConditionsStrategyImp: FlowIDSearchStrategy, + private readonly getFlowIdsFromCategoryConditionsStrategyImp: FlowIDSearchStrategy + ) {} + + async search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise { + const { flowIDs: flowIdsFromFlowObjects }: FlowIdSearchStrategyResponse = + await this.getFlowIdsFromObjectConditionsStrategyImp.search( + models, + flowObjectsConditions, + flowCategoryConditions + ); + + const { flowIDs: flowIdsFromFlowCategories }: FlowIdSearchStrategyResponse = + await this.getFlowIdsFromCategoryConditionsStrategyImp.search( + models, + flowObjectsConditions, + flowCategoryConditions + ); + + const mergeFlowIDs: FlowId[] = + mergeFlowIDsFromFilteredFlowObjectsAndFlowCategories( + flowIdsFromFlowObjects, + flowIdsFromFlowCategories + ); + + return { flowIDs: mergeFlowIDs }; + } +} diff --git a/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts b/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts new file mode 100644 index 00000000..99fbcdb2 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts @@ -0,0 +1,45 @@ +import { Database } from '@unocha/hpc-api-core/src/db'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { Service } from 'typedi'; +import { FlowObjectService } from '../../../flow-object/flow-object-service'; +import { FlowCategoryFilters } from '../../graphql/args'; +import { + FlowIDSearchStrategy, + FlowIdSearchStrategyResponse, +} from '../flowID-search-strategy'; +import { mapFlowObjectConditionsToWhereClause } from './utils'; + +@Service() +export class GetFlowIdsFromObjectConditionsStrategyImpl + implements FlowIDSearchStrategy +{ + constructor(private readonly flowObjectService: FlowObjectService) {} + + async search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise { + const flowObjectWhere = mapFlowObjectConditionsToWhereClause( + flowObjectsConditions + ); + + const flowIDsFromFilteredFlowObjects: FlowId[] = []; + const tempFlowIDs: FlowId[][] = await Promise.all( + flowObjectWhere.map((whereClause) => + this.flowObjectService.getFlowIdsFromFlowObjects(models, whereClause) + ) + ); + + // Flatten array of arrays keeping only values present in all arrays + const flowIDs = tempFlowIDs.reduce( + (a: FlowId[], b: FlowId[]) => a.filter((c) => b.includes(c)), + [] + ); + + flowIDsFromFilteredFlowObjects.push(...flowIDs); + + return { flowIDs: flowIDsFromFilteredFlowObjects.sort() }; + } +} diff --git a/src/domain-services/flows/strategy/impl/only-flow-conditions-strategy.ts b/src/domain-services/flows/strategy/impl/only-flow-conditions-strategy-impl.ts similarity index 100% rename from src/domain-services/flows/strategy/impl/only-flow-conditions-strategy.ts rename to src/domain-services/flows/strategy/impl/only-flow-conditions-strategy-impl.ts diff --git a/src/domain-services/flows/strategy/impl/utils.ts b/src/domain-services/flows/strategy/impl/utils.ts new file mode 100644 index 00000000..e7dac5ba --- /dev/null +++ b/src/domain-services/flows/strategy/impl/utils.ts @@ -0,0 +1,149 @@ +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { Cond, Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { FlowCategoryFilters } from '../../graphql/args'; + +/* + * Map structure: + * { + * KEY = objectType: string, + * VALUE = { + * KEY = refDirection: string, + * VALUE = [objectID: number] + * } + * } + */ +export function mapFlowObjectConditionsToWhereClause( + flowObjectConditions: Map> +): any[] { + const whereClauses: any = []; + for (const [objectType, refDirectionMap] of flowObjectConditions) { + for (const [refDirection, objectIDs] of refDirectionMap) { + const whereClause = { + objectID: { + [Op.IN]: objectIDs, + }, + refDirection: { + [Op.LIKE]: refDirection, + }, + objectType: { + [Op.LIKE]: objectType, + }, + }; + + whereClauses.push(whereClause); + } + } + + return whereClauses; +} + +export function mapFlowCategoryConditionsToWhereClause( + flowCategoryConditions: FlowCategoryFilters +) { + let whereClause = {}; + + if (flowCategoryConditions.pending) { + whereClause = { + group: { + [Op.IN]: ['inactiveReason'], + }, + name: { + [Op.IN]: ['Pending review'], + }, + }; + } + + if (flowCategoryConditions.categoryFilters?.length > 0) { + // Map category filters + // getting Id when possible + // or name and group otherwise + const categoryIdFilters: number[] = []; + const categoryFilters = new Map(); + for (const categoryFilter of flowCategoryConditions.categoryFilters) { + if (categoryFilter.id) { + categoryIdFilters.push(categoryFilter.id); + } else if (categoryFilter.group && categoryFilter.name) { + const group = categoryFilter.group; + const name = categoryFilter.name; + + const groupsNamesFilter = + (categoryFilters.get(group) as string[]) || []; + + groupsNamesFilter.push(name); + categoryFilters.set(group, groupsNamesFilter); + } + } + + if (categoryIdFilters.length > 0) { + whereClause = { + ...whereClause, + id: { + [Op.IN]: categoryIdFilters, + }, + }; + } + + // for each entry of the group name + // add a condition to the where clause + // with the names associated to the group + // both in the same AND clause + for (const [group, names] of categoryFilters) { + whereClause = { + ...whereClause, + [Cond.AND]: [ + { + group: { + [Op.LIKE]: group, + }, + name: { + [Op.IN]: names, + }, + }, + ], + }; + } + } + + return whereClause; +} + +export function mergeFlowIDsFromFilteredFlowObjectsAndFlowCategories( + flowIDsFromFilteredFlowObjects: FlowId[], + flowIDsFromFilteredFlowCategories: FlowId[] +): FlowId[] { + const flowIDsFromFilteredFlowCategoriesIsEmpty = + !flowIDsFromFilteredFlowCategories || + !flowIDsFromFilteredFlowCategories.length; + const flowIDsFromFilteredFlowObjectsIsEmpty = + !flowIDsFromFilteredFlowObjects || !flowIDsFromFilteredFlowObjects.length; + + if ( + flowIDsFromFilteredFlowCategoriesIsEmpty && + flowIDsFromFilteredFlowObjectsIsEmpty + ) { + return []; + } + + if ( + flowIDsFromFilteredFlowCategoriesIsEmpty && + !flowIDsFromFilteredFlowObjectsIsEmpty + ) { + return flowIDsFromFilteredFlowObjects; + } + + if ( + !flowIDsFromFilteredFlowCategoriesIsEmpty && + flowIDsFromFilteredFlowObjectsIsEmpty + ) { + return flowIDsFromFilteredFlowCategories; + } + + return flowIDsFromFilteredFlowObjects.length > + flowIDsFromFilteredFlowCategories.length + ? flowIDsFromFilteredFlowCategories.filter((flowID) => + flowIDsFromFilteredFlowObjects.includes(flowID) + ) + : flowIDsFromFilteredFlowObjects.filter((flowID) => + flowIDsFromFilteredFlowCategories.includes(flowID) + ); +} diff --git a/src/utils/graphql/pagination.ts b/src/utils/graphql/pagination.ts index b0fc2d54..797d8a85 100644 --- a/src/utils/graphql/pagination.ts +++ b/src/utils/graphql/pagination.ts @@ -17,10 +17,10 @@ export class PageInfo { hasPreviousPage: boolean; @Field({ nullable: false }) - startCursor: string; + prevPageCursor: string; @Field({ nullable: false }) - endCursor: string; + nextPageCursor: string; @Field({ nullable: false }) pageSize: number; @@ -71,10 +71,10 @@ export class PaginationArgs { limit: number; @Field({ nullable: true }) - afterCursor: string; + nextPageCursor: string; @Field({ nullable: true }) - beforeCursor: string; + prevPageCursor: string; @Field(() => String, { nullable: true }) sortField: TSortFields;