From fa06bb054b32f781ce834439d386e2795ffee945 Mon Sep 17 00:00:00 2001 From: manelcecs Date: Fri, 10 Nov 2023 13:43:39 +0100 Subject: [PATCH] WIP: strategy --- .../flows/flow-search-service.ts | 330 ++++-------------- .../flows/strategy/flow-search-strategy.ts | 6 + .../impl/only-flow-conditions-strategy.ts | 252 +++++++++++++ .../strategy/only-flow-conditions-strategy.ts | 18 - 4 files changed, 321 insertions(+), 285 deletions(-) create mode 100644 src/domain-services/flows/strategy/flow-search-strategy.ts create mode 100644 src/domain-services/flows/strategy/impl/only-flow-conditions-strategy.ts delete mode 100644 src/domain-services/flows/strategy/only-flow-conditions-strategy.ts diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index 808545ad..e38ca553 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -1,41 +1,23 @@ -import { Service } from 'typedi'; -import { - FlowParkedParentSource, - FlowSearchResult, - FlowSortField, -} from './graphql/types'; import { Database } from '@unocha/hpc-api-core/src/db/type'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; -import { OrganizationService } from '../organizations/organization-service'; -import { LocationService } from '../location/location-service'; -import { PlanService } from '../plans/plan-service'; -import { UsageYearService } from '../usage-years/usage-year-service'; -import { CategoryService } from '../categories/category-service'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; -import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; -import { FlowLinkService } from './flow-link-service'; -import { ExternalReferenceService } from '../external-reference/external-reference-service'; -import { ReportDetailService } from '../report-details/report-detail-service'; -import { FlowService } from './flow-service'; +import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; +import { Service } from 'typedi'; import { FlowObjectFilters, SearchFlowsArgs, SearchFlowsFilters, } from './graphql/args'; +import { + FlowSearchResult +} from './graphql/types'; +import { FlowSearchStrategy } from './strategy/flow-search-strategy'; +import { OnlyFlowFiltersStrategy } from './strategy/impl/only-flow-conditions-strategy'; @Service() export class FlowSearchService { constructor( - private readonly organizationService: OrganizationService, - private readonly locationService: LocationService, - private readonly planService: PlanService, - private readonly usageYearService: UsageYearService, - private readonly categoryService: CategoryService, - private readonly flowLinkService: FlowLinkService, - private readonly externalReferenceService: ExternalReferenceService, - private readonly reportDetailService: ReportDetailService, - private readonly flowService: FlowService - ) {} + private readonly onlyFlowFiltersStrategy: OnlyFlowFiltersStrategy + ) { } async search( models: Database, @@ -47,12 +29,25 @@ export class FlowSearchService { throw new Error('Cannot use before and after cursor at the same time'); } - const sortCondition = { - column: sortField, - order: sortOrder, - }; + let cursorCondition; + if (afterCursor) { + cursorCondition = { + id: { + [Op.GT]: createBrandedValue(afterCursor), + }, + }; + } else if (beforeCursor) { + cursorCondition = { + id: { + [Op.LT]: createBrandedValue(beforeCursor), + }, + }; + } - const limitComputed = limit + 1; // Fetch one more item to check for hasNextPage + const orderBy = { + column: sortField??'updatedAt', + order: sortOrder ?? 'desc', + }; const { flowFilters, flowObjectFilters } = filters; @@ -71,270 +66,51 @@ export class FlowSearchService { bothFlowFilters = true; } - const { flowObjectConditions, flowConditions } = [ - this.prepareFlowObjectConditions(flowObjectFilters), - this.prepareFlowConditions(flowFilters), - ]; + let conditions: any = { ...cursorCondition }; + const strategy = this.determineStrategy(flowFilters, flowObjectFilters, conditions); - let condition; - if (afterCursor) { - condition = { - id: { - [Op.GT]: createBrandedValue(afterCursor), - }, - }; - } else if (beforeCursor) { - condition = { - id: { - [Op.LT]: createBrandedValue(beforeCursor), - }, - }; - } - - if (filters?.activeStatus !== undefined) { - condition = { - ...condition, - activeStatus: filters.activeStatus, - }; - } - - const [flows, countRes] = await Promise.all([ - this.flowService.getFlows( - models, - condition, - sortCondition, - limitComputed - ), - this.flowService.getFlowsCount(models, condition), - ]); - - const hasNextPage = flows.length > limit; - if (hasNextPage) { - flows.pop(); // Remove the extra item used to check hasNextPage - } - - const count = countRes[0] as { count: number }; - - const flowIds: FlowId[] = flows.map((flow) => flow.id); - - const organizationsFO: any[] = []; - const locationsFO: any[] = []; - const plansFO: any[] = []; - const usageYearsFO: any[] = []; - - const [externalReferencesMap] = await Promise.all([ - this.externalReferenceService.getExternalReferencesForFlows( - flowIds, - models - ), - this.getFlowObjects( - flowIds, - models, - organizationsFO, - locationsFO, - plansFO, - usageYearsFO - ), - ]); - - const flowLinksMap = await this.flowLinkService.getFlowLinksForFlows( - flowIds, - models - ); - - const [ - categoriesMap, - organizationsMap, - locationsMap, - plansMap, - usageYearsMap, - reportDetailsMap, - ] = await Promise.all([ - this.categoryService.getCategoriesForFlows(flowLinksMap, models), - this.organizationService.getOrganizationsForFlows( - organizationsFO, - models - ), - this.locationService.getLocationsForFlows(locationsFO, models), - this.planService.getPlansForFlows(plansFO, models), - this.usageYearService.getUsageYearsForFlows(usageYearsFO, models), - this.reportDetailService.getReportDetailsForFlows(flowIds, models), - ]); - - const items = flows.map((flow) => { - const flowLink = flowLinksMap.get(flow.id) || []; - const categories = categoriesMap.get(flow.id) || []; - const organizations = organizationsMap.get(flow.id) || []; - const locations = [...(locationsMap.get(flow.id) || [])]; - const plans = plansMap.get(flow.id) || []; - const usageYears = usageYearsMap.get(flow.id) || []; - const externalReferences = externalReferencesMap.get(flow.id) || []; - const reportDetails = reportDetailsMap.get(flow.id) || []; - - const parkedParentSource: FlowParkedParentSource[] = []; - if (flow.activeStatus && flowLink.length > 0) { - this.getParketParents(flow, flowLink, models, parkedParentSource); - } + return await strategy.search(conditions, orderBy, limit, cursorCondition, models); - const childIDs: number[] = flowLinksMap - .get(flow.id) - ?.filter( - (flowLink) => flowLink.parentID === flow.id && flowLink.depth > 0 - ) - .map((flowLink) => flowLink.childID.valueOf()) as number[]; - - const parentIDs: number[] = flowLinksMap - .get(flow.id) - ?.filter( - (flowLink) => flowLink.childID === flow.id && flowLink.depth > 0 - ) - .map((flowLink) => flowLink.parentID.valueOf()) as number[]; - - return { - // Mandatory fields - id: flow.id.valueOf(), - versionID: flow.versionID, - amountUSD: flow.amountUSD.toString(), - createdAt: flow.createdAt.toISOString(), - updatedAt: flow.updatedAt.toISOString(), - activeStatus: flow.activeStatus, - restricted: flow.restricted, - // Optional fields - categories, - organizations, - locations, - plans, - usageYears, - childIDs, - parentIDs, - origAmount: flow.origAmount ? flow.origAmount.toString() : '', - origCurrency: flow.origCurrency ? flow.origCurrency.toString() : '', - externalReferences, - reportDetails, - parkedParentSource, - // Paged item field - cursor: flow.id.valueOf(), - }; - }); - - return { - flows: items, - hasNextPage: limit <= flows.length, - hasPreviousPage: afterCursor !== undefined, - startCursor: flows.length ? flows[0].id.valueOf() : 0, - endCursor: flows.length ? flows[flows.length - 1].id.valueOf() : 0, - pageSize: flows.length, - sortField: sortCondition.column, - sortOrder: sortCondition.order, - total: count.count, - }; - } - - private async getFlowObjects( - flowIds: FlowId[], - models: Database, - organizationsFO: any[], - locationsFO: any[], - plansFO: any[], - usageYearsFO: any[] - ): Promise { - const flowObjects = await models.flowObject.find({ - where: { - flowID: { - [Op.IN]: flowIds, - }, - }, - }); - - flowObjects.forEach((flowObject) => { - if (flowObject.objectType === 'organization') { - organizationsFO.push(flowObject); - } else if (flowObject.objectType === 'location') { - locationsFO.push(flowObject); - } else if (flowObject.objectType === 'plan') { - plansFO.push(flowObject); - } else if (flowObject.objectType === 'usageYear') { - usageYearsFO.push(flowObject); - } - }); } - private async getParketParents( - flow: any, - flowLink: any[], - models: Database, - parkedParentSource: FlowParkedParentSource[] - ): Promise { - const flowLinksDepth0 = flowLink.filter((flowLink) => flowLink.depth === 0); - - const flowLinksParent = flowLinksDepth0.filter( - (flowLink) => flowLink.parentID === flow.id - ); - - const parentFlowIds = flowLinksParent.map((flowLink) => - flowLink.parentID.valueOf() - ); - - const categories = await models.category.find({ - where: { - group: 'flowType', - name: 'parked', - }, - }); - - const categoriesIDs = categories.map((category) => category.id); - - const categoryRef = await models.categoryRef.find({ - where: { - categoryID: { - [Op.IN]: categoriesIDs, - }, - versionID: flow.versionID, - }, - }); + - const parentFlows = flowLinksParent.filter((flowLink) => { - return categoryRef.some( - (categoryRef) => - categoryRef.objectID.valueOf() === flowLink.parentID.valueOf() - ); - }); - } + private prepareFlowObjectConditions( flowObjectFilters: FlowObjectFilters[] ): Map> { const flowObjectConditions = new Map>(); - + for (const flowObjectFilter of flowObjectFilters || []) { const objectType = flowObjectFilter.objectType; const direction = flowObjectFilter.direction; const objectID = flowObjectFilter.objectID; - + // Ensure the map for the objectType is initialized if (!flowObjectConditions.has(objectType)) { flowObjectConditions.set(objectType, new Map()); } - + const flowObjectCondition = flowObjectConditions.get(objectType); - + // Ensure the map for the direction is initialized if (!flowObjectCondition!.has(direction)) { flowObjectCondition!.set(direction, []); } - + const flowObjectDirectionCondition = flowObjectCondition!.get(direction); - + // Add the objectID to the array flowObjectDirectionCondition!.push(objectID); } - + return flowObjectConditions; } - + private prepareFlowConditions(flowFilters: SearchFlowsFilters): Map { const flowConditions = new Map(); - + if (flowFilters) { Object.entries(flowFilters).forEach(([key, value]) => { if (value !== undefined) { @@ -342,8 +118,28 @@ export class FlowSearchService { } }); } - + return flowConditions; } - + + private determineStrategy(flowFilters: SearchFlowsFilters, flowObjectFilters: FlowObjectFilters[], conditions: any): FlowSearchStrategy { + if ((!flowFilters && (!flowObjectFilters || flowObjectFilters.length === 0)) || (flowFilters && (!flowObjectFilters || flowObjectFilters.length === 0))) { + const flowConditions = this.prepareFlowConditions(flowFilters); + conditions = { ...conditions, ...flowConditions } + return this.onlyFlowFiltersStrategy; + } + // else if (!flowFilters && flowObjectFilters.length !== 0) { + // const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); + // conditions = {...conditions, ...flowObjectConditions} + // return new OnlyFlowObjectFiltersStrategy(this); + // } else if (flowFilters && flowObjectFilters.length !== 0) { + // const flowConditions = this.prepareFlowConditions(flowFilters); + // const flowObjectConditions = this.prepareFlowObjectConditions(flowObjectFilters); + // conditions = {...conditions, ...flowConditions, ...flowObjectConditions} + // return new BothFlowFiltersStrategy(this); + // } + + throw new Error('Invalid combination of flowFilters and flowObjectFilters - temp: only provide flowFilters'); + } + } diff --git a/src/domain-services/flows/strategy/flow-search-strategy.ts b/src/domain-services/flows/strategy/flow-search-strategy.ts new file mode 100644 index 00000000..b5d844a1 --- /dev/null +++ b/src/domain-services/flows/strategy/flow-search-strategy.ts @@ -0,0 +1,6 @@ +import { Database } from "@unocha/hpc-api-core/src/db"; +import { FlowSearchResult } from "../graphql/types"; + +export interface FlowSearchStrategy{ + search(flowConditions: Map, orderBy: any, limit: number, cursorCondition: any, models: Database): Promise; +} \ No newline at end of file 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.ts new file mode 100644 index 00000000..652f0843 --- /dev/null +++ b/src/domain-services/flows/strategy/impl/only-flow-conditions-strategy.ts @@ -0,0 +1,252 @@ +import { Database } from '@unocha/hpc-api-core/src/db'; +import { Ctx } from 'type-graphql'; +import { Service } from 'typedi'; +import Context from '../../../Context'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { CategoryService } from '../../../categories/category-service'; +import { ExternalReferenceService } from '../../../external-reference/external-reference-service'; +import { LocationService } from '../../../location/location-service'; +import { OrganizationService } from '../../../organizations/organization-service'; +import { PlanService } from '../../../plans/plan-service'; +import { ReportDetailService } from '../../../report-details/report-detail-service'; +import { UsageYearService } from '../../../usage-years/usage-year-service'; +import { FlowLinkService } from '../../flow-link-service'; +import { FlowService } from '../../flow-service'; +import { FlowParkedParentSource, FlowSearchResult } from '../../graphql/types'; +import { FlowSearchStrategy } from '../flow-search-strategy'; +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; + +@Service() +export class OnlyFlowFiltersStrategy implements FlowSearchStrategy { + + + constructor( + private readonly organizationService: OrganizationService, + private readonly locationService: LocationService, + private readonly planService: PlanService, + private readonly usageYearService: UsageYearService, + private readonly categoryService: CategoryService, + private readonly flowLinkService: FlowLinkService, + private readonly externalReferenceService: ExternalReferenceService, + private readonly reportDetailService: ReportDetailService, + private readonly flowService: FlowService) { + } + + async search(flowConditions: Map, orderBy: any, limit: number, cursorCondition: any, models: Database): Promise { + + // Fetch one more item to check for hasNextPage + const limitComputed = limit + 1; + + // Build conditions object + const conditions: any = { ...cursorCondition }; + + if (flowConditions.size > 0) { + flowConditions.forEach((value, key) => { + conditions[key] = value; + }); + } + + const [flows, countRes] = await Promise.all([ + this.flowService.getFlows( + models, + conditions, + orderBy, + limitComputed + ), + this.flowService.getFlowsCount(models, conditions), + ]); + + const hasNextPage = flows.length > limit; + if (hasNextPage) { + flows.pop(); // Remove the extra item used to check hasNextPage + } + + const count = countRes[0] as { count: number }; + + const flowIds: FlowId[] = flows.map((flow) => flow.id); + + const organizationsFO: any[] = []; + const locationsFO: any[] = []; + const plansFO: any[] = []; + const usageYearsFO: any[] = []; + + const [externalReferencesMap] = await Promise.all([ + this.externalReferenceService.getExternalReferencesForFlows( + flowIds, + models + ), + this.getFlowObjects( + flowIds, + models, + organizationsFO, + locationsFO, + plansFO, + usageYearsFO + ), + ]); + + const flowLinksMap = await this.flowLinkService.getFlowLinksForFlows( + flowIds, + models + ); + + const [ + categoriesMap, + organizationsMap, + locationsMap, + plansMap, + usageYearsMap, + reportDetailsMap, + ] = await Promise.all([ + this.categoryService.getCategoriesForFlows(flowLinksMap, models), + this.organizationService.getOrganizationsForFlows( + organizationsFO, + models + ), + this.locationService.getLocationsForFlows(locationsFO, models), + this.planService.getPlansForFlows(plansFO, models), + this.usageYearService.getUsageYearsForFlows(usageYearsFO, models), + this.reportDetailService.getReportDetailsForFlows(flowIds, models), + ]); + + const items = flows.map((flow) => { + const flowLink = flowLinksMap.get(flow.id) || []; + const categories = categoriesMap.get(flow.id) || []; + const organizations = organizationsMap.get(flow.id) || []; + const locations = [...(locationsMap.get(flow.id) || [])]; + const plans = plansMap.get(flow.id) || []; + const usageYears = usageYearsMap.get(flow.id) || []; + const externalReferences = externalReferencesMap.get(flow.id) || []; + const reportDetails = reportDetailsMap.get(flow.id) || []; + + const parkedParentSource: FlowParkedParentSource[] = []; + if (flow.activeStatus && flowLink.length > 0) { + this.getParketParents(flow, flowLink, models, parkedParentSource); + } + + const childIDs: number[] = flowLinksMap + .get(flow.id) + ?.filter( + (flowLink) => flowLink.parentID === flow.id && flowLink.depth > 0 + ) + .map((flowLink) => flowLink.childID.valueOf()) as number[]; + + const parentIDs: number[] = flowLinksMap + .get(flow.id) + ?.filter( + (flowLink) => flowLink.childID === flow.id && flowLink.depth > 0 + ) + .map((flowLink) => flowLink.parentID.valueOf()) as number[]; + + return { + // Mandatory fields + id: flow.id.valueOf(), + versionID: flow.versionID, + amountUSD: flow.amountUSD.toString(), + createdAt: flow.createdAt.toISOString(), + updatedAt: flow.updatedAt.toISOString(), + activeStatus: flow.activeStatus, + restricted: flow.restricted, + // Optional fields + categories, + organizations, + locations, + plans, + usageYears, + childIDs, + parentIDs, + origAmount: flow.origAmount ? flow.origAmount.toString() : '', + origCurrency: flow.origCurrency ? flow.origCurrency.toString() : '', + externalReferences, + reportDetails, + parkedParentSource, + // Paged item field + cursor: flow.id.valueOf(), + }; + }); + + return { + flows: items, + hasNextPage: limit <= flows.length, + hasPreviousPage: false,// TODO: cursorCondition['id'].GT !== undefined, + startCursor: flows.length ? flows[0].id.valueOf() : 0, + endCursor: flows.length ? flows[flows.length - 1].id.valueOf() : 0, + pageSize: flows.length, + sortField: orderBy.column, + sortOrder: orderBy.order, + total: count.count, + }; + } + + private async getFlowObjects( + flowIds: FlowId[], + models: Database, + organizationsFO: any[], + locationsFO: any[], + plansFO: any[], + usageYearsFO: any[] + ): Promise { + const flowObjects = await models.flowObject.find({ + where: { + flowID: { + [Op.IN]: flowIds, + }, + }, + }); + + flowObjects.forEach((flowObject) => { + if (flowObject.objectType === 'organization') { + organizationsFO.push(flowObject); + } else if (flowObject.objectType === 'location') { + locationsFO.push(flowObject); + } else if (flowObject.objectType === 'plan') { + plansFO.push(flowObject); + } else if (flowObject.objectType === 'usageYear') { + usageYearsFO.push(flowObject); + } + }); + } + + private async getParketParents( + flow: any, + flowLink: any[], + models: Database, + parkedParentSource: FlowParkedParentSource[] + ): Promise { + const flowLinksDepth0 = flowLink.filter((flowLink) => flowLink.depth === 0); + + const flowLinksParent = flowLinksDepth0.filter( + (flowLink) => flowLink.parentID === flow.id + ); + + const parentFlowIds = flowLinksParent.map((flowLink) => + flowLink.parentID.valueOf() + ); + + const categories = await models.category.find({ + where: { + group: 'flowType', + name: 'parked', + }, + }); + + const categoriesIDs = categories.map((category) => category.id); + + const categoryRef = await models.categoryRef.find({ + where: { + categoryID: { + [Op.IN]: categoriesIDs, + }, + versionID: flow.versionID, + }, + }); + + const parentFlows = flowLinksParent.filter((flowLink) => { + return categoryRef.some( + (categoryRef) => + categoryRef.objectID.valueOf() === flowLink.parentID.valueOf() + ); + }); + } + +} \ No newline at end of file diff --git a/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts b/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts deleted file mode 100644 index ecac6afd..00000000 --- a/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Database } from '@unocha/hpc-api-core/src/db'; -import { Ctx } from 'type-graphql'; -import { Service } from 'typedi'; -import Context from '../../Context'; - -@Service() -export class OnlyFlowFiltersStrategy { - - private models: Database; - - constructor(@Ctx() context: Context) { - this.models = context.models; - } - - search(flowConditions: Map, orderBy: any, limit: number) { - - } -} \ No newline at end of file