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..3a410d6d 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 { + type 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,49 +277,62 @@ 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) { + return { + strategy: this.onlyFlowFiltersStrategy, + conditions: flowConditions, + }; + } else if ( + !isFlowFilterDefined && + isFlowObjectFiltersNotEmpty && + isFlowCategoryFilterDefined + ) { const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); - conditions = { ...conditions, ...flowObjectConditions }; return { strategy: this.flowObjectFiltersStrategy, - conditions: this.buildConditionsMap(undefined, flowObjectConditions), + conditions: { + conditionsMap: this.buildConditionsMap({}, flowObjectConditions), + flowCategoryFilters, + }, }; - } else if (isFlowFilterDefined && isFlowObjectFiltersNotEmpty) { + } else if ( + isFlowFilterDefined && + isFlowObjectFiltersNotEmpty && + isFlowCategoryFilterDefined + ) { const flowConditions = this.prepareFlowConditions(flowFilters); const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); - conditions = { - ...conditions, - ...flowConditions, - ...flowObjectConditions, - }; return { strategy: this.flowObjectFiltersStrategy, - conditions: this.buildConditionsMap( - flowConditions, - flowObjectConditions - ), + conditions: { + conditionsMap: this.buildConditionsMap( + flowConditions, + flowObjectConditions + ), + flowCategoryFilters, + }, }; } @@ -469,14 +485,15 @@ export class FlowSearchService { parkedParentSource: FlowParkedParentSource[], sortColumn?: FlowSortField ): FlowPaged { - let cursor: string | number | Date = sortColumn - ? flow.id.valueOf() - : flow[sortColumn ?? 'updatedAt']; + let cursor = sortColumn ? flow[sortColumn] : flow.updatedAt; if (cursor instanceof Date) { cursor = cursor.toISOString(); } else if (typeof cursor === 'number') { cursor = cursor.toString(); + } else if (typeof cursor === 'boolean' || cursor === null) { + // cases such as 'boolean' + cursor = flow.id.toString(); } return { @@ -510,11 +527,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 +562,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 +572,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..4a721325 --- /dev/null +++ b/src/domain-services/flows/strategy/flowID-search-strategy.ts @@ -0,0 +1,17 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { type FlowCategoryFilters } from '../graphql/args'; + +export interface FlowIdSearchStrategyResponse { + flowIDs: FlowId[]; +} + +export interface FlowIDSearchStrategy { + search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise; + + generateWhereClause(flowIds: FlowId[], conditions: any): any; +} 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..2a458701 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/flow-object-conditions-strategy-impl.ts @@ -0,0 +1,134 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { Cond } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { Service } from 'typedi'; +import { FlowService } from '../../flow-service'; +import { type FlowCategoryFilters } from '../../graphql/args'; +import { + type FlowSearchStrategy, + type FlowSearchStrategyResponse, +} from '../flow-search-strategy'; +import { + type FlowIDSearchStrategy, + type 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 + ); + + // Combine conditions from flowObjects FlowIDs and flow conditions + const countConditions = { + [Cond.AND]: [ + flowEntityConditions ?? {}, + + searchFlowIdsStrategy.generateWhereClause( + flowIdsToFilter, + flowCategoryConditions + ), + ], + }; + + // Combine cursor condition with flow conditions + const searchConditions = { + [Cond.AND]: [ + flowEntityConditions ?? {}, + cursorCondition ?? {}, + searchFlowIdsStrategy.generateWhereClause( + flowIdsToFilter, + flowCategoryConditions + ), + ], + }; + + // 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 }; + } + + // 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 isFlowObjectsConditionsIsDefined = + flowObjectsConditions !== undefined; + const isFlowCategoryConditionsIsDefined = + flowCategoryConditions !== undefined; + + const flowObjectsConditionsIsNotEmpty = + isFlowObjectsConditionsIsDefined && flowObjectsConditions.size; + const flowCategoryConditionsIsNotEmpty = + isFlowCategoryConditionsIsDefined && + 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' + ); + } +} 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..1225e431 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-category-conditions-strategy-impl.ts @@ -0,0 +1,67 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type CategoryId } from '@unocha/hpc-api-core/src/db/models/category'; +import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +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 { type FlowCategoryFilters } from '../../graphql/args'; +import { + type FlowIDSearchStrategy, + type 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 = [ + ...new Set(categoryRefs.map((categoryRef) => categoryRef.objectID)), + ].map((flowId) => createBrandedValue(flowId)); + + return { flowIDs: flowIds }; + } + + generateWhereClause( + flowIds: FlowId[], + flowCategoryConditions: FlowCategoryFilters + ) { + const operation = flowCategoryConditions.pending ? Op.IN : Op.NOT_IN; + return { + id: { + [operation]: 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..99ff6626 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-mixed-conditions-strategy-impl.ts @@ -0,0 +1,57 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type 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 { type FlowCategoryFilters } from '../../graphql/args'; +import { + type FlowIDSearchStrategy, + type FlowIdSearchStrategyResponse, +} from '../flowID-search-strategy'; +import { GetFlowIdsFromCategoryConditionsStrategyImpl } from './get-flowIds-flow-category-conditions-strategy-impl'; +import { GetFlowIdsFromObjectConditionsStrategyImpl } from './get-flowIds-flow-object-conditions-strategy-impl'; +import { mergeFlowIDsFromFilteredFlowObjectsAndFlowCategories } from './utils'; + +@Service() +export class GetFlowIdsFromMixedConditionsStrategyImpl + implements FlowIDSearchStrategy +{ + constructor( + private readonly getFlowIdsFromObjectConditionsStrategy: GetFlowIdsFromObjectConditionsStrategyImpl, + private readonly getFlowIdsFromCategoryConditionsStrategy: GetFlowIdsFromCategoryConditionsStrategyImpl + ) {} + + async search( + models: Database, + flowObjectsConditions: Map>, + flowCategoryConditions: FlowCategoryFilters + ): Promise { + const { flowIDs: flowIdsFromFlowObjects }: FlowIdSearchStrategyResponse = + await this.getFlowIdsFromObjectConditionsStrategy.search( + models, + flowObjectsConditions + ); + + const { flowIDs: flowIdsFromFlowCategories }: FlowIdSearchStrategyResponse = + await this.getFlowIdsFromCategoryConditionsStrategy.search( + models, + flowObjectsConditions, + flowCategoryConditions + ); + + const mergeFlowIDs: FlowId[] = + mergeFlowIDsFromFilteredFlowObjectsAndFlowCategories( + flowIdsFromFlowObjects, + flowIdsFromFlowCategories + ); + + return { flowIDs: mergeFlowIDs }; + } + + generateWhereClause(flowIds: FlowId[]) { + return { + id: { + [Op.IN]: flowIds, + }, + }; + } +} 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..974436f5 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/get-flowIds-flow-object-conditions-strategy-impl.ts @@ -0,0 +1,47 @@ +import { type Database } from '@unocha/hpc-api-core/src/db'; +import { type 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 { + type FlowIDSearchStrategy, + type 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> + ): 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.flat(); + flowIDsFromFilteredFlowObjects.push(...new Set(flowIDs)); + + return { flowIDs: flowIDsFromFilteredFlowObjects }; + } + + generateWhereClause(flowIds: FlowId[]) { + return { + id: { + [Op.IN]: flowIds, + }, + }; + } +} 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..16fa0e84 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/utils.ts @@ -0,0 +1,144 @@ +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 { type 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 !== undefined) { + whereClause = { + group: 'inactiveReason', + name: '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 isFlowIDsFromFilteredFlowCategoriesIsEmpty = + !flowIDsFromFilteredFlowCategories?.length; + const isFlowIDsFromFilteredFlowObjectsIsEmpty = + !flowIDsFromFilteredFlowObjects?.length; + + if ( + isFlowIDsFromFilteredFlowCategoriesIsEmpty && + isFlowIDsFromFilteredFlowObjectsIsEmpty + ) { + return []; + } + + if ( + isFlowIDsFromFilteredFlowCategoriesIsEmpty && + !isFlowIDsFromFilteredFlowObjectsIsEmpty + ) { + return flowIDsFromFilteredFlowObjects; + } + + if ( + !isFlowIDsFromFilteredFlowCategoriesIsEmpty && + isFlowIDsFromFilteredFlowObjectsIsEmpty + ) { + 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;