diff --git a/src/domain-services/categories/category-service.ts b/src/domain-services/categories/category-service.ts index 76f7307d..fb0f70de 100644 --- a/src/domain-services/categories/category-service.ts +++ b/src/domain-services/categories/category-service.ts @@ -3,14 +3,20 @@ 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 { FlowCategory } from '../flows/graphql/types'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; +import { InstanceDataOfModel } from '@unocha/hpc-api-core/src/db/util/raw-model'; @Service() export class CategoryService { - async getFlowCategory(flow: any, models: Database): Promise<FlowCategory[]> { - const flowIdBranded = createBrandedValue(flow.id); + async getCategoriesForFlows( + flowsIds: FlowId[], + models: Database + ): Promise<Map<number, FlowCategory[]>> { const flowLinks = await models.flowLink.find({ where: { - childID: flowIdBranded, + childID: { + [Op.IN]: flowsIds, + }, }, }); @@ -23,7 +29,6 @@ export class CategoryService { objectID: { [Op.IN]: flowLinksBrandedIds, }, - versionID: flow.versionID, }, }); @@ -35,10 +40,36 @@ export class CategoryService { }, }); - return categories.map((cat) => ({ - id: cat.id, - name: cat.name, - group: cat.group, - })); + // Group categories by flow ID for easy mapping + const categoriesMap = new Map<number, FlowCategory[]>(); + + // Populate the map with categories for each flow + categoriesRef.forEach((catRef) => { + const flowId = catRef.objectID.valueOf(); + + if (!categoriesMap.has(flowId)) { + categoriesMap.set(flowId, []); + } + + const categoriesForFlow = categoriesMap.get(flowId)!; + + const category = categories.find((cat) => cat.id === catRef.categoryID); + + if (!category) { + throw new Error(`Category with ID ${catRef.categoryID} does not exist`); + } + + categoriesForFlow.push(this.mapCategoryToFlowCategory(category)); + }); + + return categoriesMap; } + + private mapCategoryToFlowCategory = ( + category: InstanceDataOfModel<Database['category']> + ): FlowCategory => ({ + id: category.id, + name: category.name, + group: category.group, + }); } diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index 956048bf..52df18e0 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -1,13 +1,5 @@ import { Service } from 'typedi'; -import { - FlowCategory, - FlowLocation, - FlowOrganization, - FlowPlan, - FlowSearchResult, - FlowSortField, - FlowUsageYear, -} from './graphql/types'; +import { 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'; @@ -15,7 +7,8 @@ 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 { prepareConditionFromCursor } from '../../utils/graphql/pagination'; +import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; @Service() export class FlowSearchService { @@ -29,7 +22,7 @@ export class FlowSearchService { async search( models: Database, - first: number, + limit: number, afterCursor?: number, beforeCursor?: number, sortField?: FlowSortField, @@ -44,93 +37,107 @@ export class FlowSearchService { order: sortOrder ?? 'desc', }; - let flows; - const countRes = await models.flow.count(); - const count = countRes[0] as { count: number }; - - const hasCursor = afterCursor || beforeCursor; - - if (hasCursor) { - const condition = prepareConditionFromCursor( - sortCondition, - afterCursor, - beforeCursor - ); + const limitComputed = limit + 1; // Fetch one more item to check for hasNextPage - flows = await models.flow.find({ - orderBy: sortCondition, - limit: first, - where: { - ...condition, + let condition; + if (afterCursor) { + condition = { + id: { + [Op.GT]: createBrandedValue(afterCursor), }, - }); - } else { - flows = await models.flow.find({ + }; + } else if (beforeCursor) { + condition = { + id: { + [Op.GT]: createBrandedValue(beforeCursor), + }, + }; + } + condition = { + ...condition, + activeStatus: true, + }; + + const [flowsIds, countRes] = await Promise.all([ + models.flow.find({ orderBy: sortCondition, - limit: first, - }); + limit: limitComputed, + where: condition, + }), + models.flow.count(), + ]); + + const hasNextPage = flowsIds.length > limit; + if (hasNextPage) { + flowsIds.pop(); // Remove the extra item used to check hasNextPage } - const items = await Promise.all( - flows.map(async (flow) => { - const categories: FlowCategory[] = - await this.categoryService.getFlowCategory(flow, models); - - const organizationsFO: any[] = []; - const locationsFO: any[] = []; - const plansFO: any[] = []; - const usageYearsFO: any[] = []; - - await this.getFlowObjects( - flow, - models, - organizationsFO, - locationsFO, - plansFO, - usageYearsFO - ); - - const organizationsPromise: Promise<FlowOrganization[]> = - this.organizationService.getFlowObjectOrganizations( - organizationsFO, - models - ); - - const locationsPromise: Promise<FlowLocation[]> = - this.locationService.getFlowObjectLocations(locationsFO, models); - - const plansPromise: Promise<FlowPlan[]> = - this.planService.getFlowObjectPlans(plansFO, models); - - const usageYearsPromise: Promise<FlowUsageYear[]> = - this.usageYearService.getFlowObjectUsageYears(usageYearsFO, models); - - const [organizations, locations, plans, usageYears] = await Promise.all( - [ - organizationsPromise, - locationsPromise, - plansPromise, - usageYearsPromise, - ] - ); - - return { - id: flow.id.valueOf(), - amountUSD: flow.amountUSD.toString(), - createdAt: flow.createdAt, - categories: categories, - organizations: organizations, - locations: locations, - plans: plans, - usageYears: usageYears, - cursor: flow.id.valueOf(), - }; - }) + const count = countRes[0] as { count: number }; + + const flowIdsList = flowsIds.map((flow) => flow.id); + + const organizationsFO: any[] = []; + const locationsFO: any[] = []; + const plansFO: any[] = []; + const usageYearsFO: any[] = []; + + await this.getFlowObjects( + flowIdsList, + models, + organizationsFO, + locationsFO, + plansFO, + usageYearsFO ); + const [ + flows, + categoriesMap, + organizationsMap, + locationsMap, + plansMap, + usageYearsMap, + ] = await Promise.all([ + models.flow.find({ + where: { + id: { + [Op.IN]: flowIdsList, + }, + }, + }), + this.categoryService.getCategoriesForFlows(flowIdsList, models), + this.organizationService.getOrganizationsForFlows( + organizationsFO, + models + ), + this.locationService.getLocationsForFlows(locationsFO, models), + this.planService.getPlansForFlows(plansFO, models), + this.usageYearService.getUsageYearsForFlows(usageYearsFO, models), + ]); + + const items = flows.map((flow) => { + 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) || []; + + return { + id: flow.id.valueOf(), + amountUSD: flow.amountUSD.toString(), + createdAt: flow.createdAt, + categories, + organizations, + locations, + plans, + usageYears, + cursor: flow.id.valueOf(), + }; + }); + return { flows: items, - hasNextPage: first <= flows.length, + 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, @@ -142,17 +149,18 @@ export class FlowSearchService { } private async getFlowObjects( - flow: any, + flowIds: FlowId[], models: Database, organizationsFO: any[], locationsFO: any[], plansFO: any[], usageYearsFO: any[] ): Promise<void> { - const flowIdBranded = createBrandedValue(flow.id); const flowObjects = await models.flowObject.find({ where: { - flowID: flowIdBranded, + flowID: { + [Op.IN]: flowIds, + }, }, }); diff --git a/src/domain-services/flows/graphql/resolver.ts b/src/domain-services/flows/graphql/resolver.ts index b78c4393..890c7d55 100644 --- a/src/domain-services/flows/graphql/resolver.ts +++ b/src/domain-services/flows/graphql/resolver.ts @@ -12,7 +12,7 @@ export default class FlowResolver { @Query(() => FlowSearchResult) async searchFlows( @Ctx() context: Context, - @Arg('first', { nullable: false }) first: number, + @Arg('limit', { nullable: false }) limit: number, @Arg('afterCursor', { nullable: true }) afterCursor: number, @Arg('beforeCursor', { nullable: true }) beforeCursor: number, @Arg('sortField', { nullable: true }) @@ -41,7 +41,7 @@ export default class FlowResolver { ): Promise<FlowSearchResult> { return await this.flowSearchService.search( context.models, - first, + limit, afterCursor, beforeCursor, sortField, diff --git a/src/domain-services/location/location-service.ts b/src/domain-services/location/location-service.ts index fc9c6739..c46a8b6d 100644 --- a/src/domain-services/location/location-service.ts +++ b/src/domain-services/location/location-service.ts @@ -29,10 +29,10 @@ export class LocationService { }); } - async getFlowObjectLocations( + async getLocationsForFlows( locationsFO: any[], models: Database - ): Promise<FlowLocation[]> { + ): Promise<Map<number, FlowLocation[]>> { const locations = await models.location.find({ where: { id: { @@ -41,9 +41,28 @@ export class LocationService { }, }); - return locations.map((loc) => ({ - id: loc.id.valueOf(), - name: loc.name!, - })); + const locationsMap = new Map<number, FlowLocation[]>(); + + locationsFO.forEach((locFO) => { + const flowId = locFO.flowID; + if (!locationsMap.has(flowId)) { + locationsMap.set(flowId, []); + } + const location = locations.find((loc) => loc.id === locFO.objectID); + + if (!location) { + throw new Error(`Location with ID ${locFO.objectID} does not exist`); + } + const locationMapped = this.mapLocationsToFlowLocations(location); + locationsMap.get(flowId)!.push(locationMapped); + }); + return locationsMap; + } + + private mapLocationsToFlowLocations(location: any) { + return { + id: location.id, + name: location.name, + }; } } diff --git a/src/domain-services/organizations/organization-service.ts b/src/domain-services/organizations/organization-service.ts index 66805bb8..215045ff 100644 --- a/src/domain-services/organizations/organization-service.ts +++ b/src/domain-services/organizations/organization-service.ts @@ -4,7 +4,7 @@ import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; @Service() export class OrganizationService { - async getFlowObjectOrganizations(organizationsFO: any[], models: Database) { + async getOrganizationsForFlows(organizationsFO: any[], models: Database) { const organizations = await models.organization.find({ where: { id: { @@ -13,11 +13,46 @@ export class OrganizationService { }, }); - return organizations.map((org) => ({ - id: org.id, - refDirection: organizationsFO.find((orgFO) => orgFO.objectID === org.id) - .refDirection, - name: org.name, - })); + const organizationsMap = new Map<number, any>(); + + organizationsFO.forEach((orgFO) => { + const flowId = orgFO.flowID; + if (!organizationsMap.has(flowId)) { + organizationsMap.set(flowId, []); + } + const organization = organizations.find( + (org) => org.id === orgFO.objectID + ); + + if (!organization) { + throw new Error( + `Organization with ID ${orgFO.objectID} does not exist` + ); + } + organizationsMap.get(flowId)!.push(organization); + }); + organizations.forEach((org) => { + const refDirection = organizationsFO.find( + (orgFO) => orgFO.objectID === org.id + ).refDirection; + + organizationsMap.set( + org.id.valueOf(), + this.mapOrganizationsToOrganizationFlows(org, refDirection) + ); + }); + + return organizationsMap; + } + + private mapOrganizationsToOrganizationFlows( + organization: any, + refDirection: any + ) { + return { + id: organization.id, + refDirection: refDirection, + name: organization.name, + }; } } diff --git a/src/domain-services/plans/plan-service.ts b/src/domain-services/plans/plan-service.ts index e39283b1..33430d04 100644 --- a/src/domain-services/plans/plan-service.ts +++ b/src/domain-services/plans/plan-service.ts @@ -47,10 +47,10 @@ export class PlanService { return years.map((y) => y.year); } - async getFlowObjectPlans( + async getPlansForFlows( plansFO: any[], models: Database - ): Promise<FlowPlan[]> { + ): Promise<Map<number, FlowPlan[]>> { const plans = await models.plan.find({ where: { id: { @@ -59,7 +59,7 @@ export class PlanService { }, }); - const flowPlans: FlowPlan[] = []; + const plansMap = new Map<number, FlowPlan[]>(); for (const plan of plans) { const planVersion = await models.planVersion.find({ @@ -69,12 +69,22 @@ export class PlanService { }, }); - flowPlans.push({ + const planMapped = { id: plan.id.valueOf(), name: planVersion[0].name, - }); + }; + + const flowId = plansFO.find( + (planFO) => planFO.objectID === plan.id + ).flowID; + + if (!plansMap.has(flowId)) { + plansMap.set(flowId, []); + } + + plansMap.get(flowId)!.push(planMapped); } - return flowPlans; + return plansMap; } } diff --git a/src/domain-services/usage-years/usage-year-service.ts b/src/domain-services/usage-years/usage-year-service.ts index 7712f235..e09197d2 100644 --- a/src/domain-services/usage-years/usage-year-service.ts +++ b/src/domain-services/usage-years/usage-year-service.ts @@ -5,10 +5,10 @@ import { FlowUsageYear } from '../flows/graphql/types'; @Service() export class UsageYearService { - async getFlowObjectUsageYears( + async getUsageYearsForFlows( usageYearsFO: any[], models: Database - ): Promise<FlowUsageYear[]> { + ): Promise<Map<number, FlowUsageYear[]>> { const usageYears = await models.usageYear.find({ where: { id: { @@ -17,11 +17,36 @@ export class UsageYearService { }, }); - return usageYears.map((usageYear) => ({ + const usageYearsMap = new Map<number, FlowUsageYear[]>(); + + usageYearsFO.forEach((usageYearFO) => { + const flowId = usageYearFO.flowID; + if (!usageYearsMap.has(flowId)) { + usageYearsMap.set(flowId, []); + } + const usageYear = usageYears.find( + (usageYear) => usageYear.id === usageYearFO.objectID + ); + + if (!usageYear) { + throw new Error( + `Usage year with ID ${usageYearFO.objectID} does not exist` + ); + } + const usageYearMapped = this.mapUsageYearsToFlowUsageYears( + usageYear, + usageYearFO.refDirection + ); + usageYearsMap.get(flowId)!.push(usageYearMapped); + }); + + return usageYearsMap; + } + + private mapUsageYearsToFlowUsageYears(usageYear: any, refDirection: any) { + return { year: usageYear.year, - direction: usageYearsFO.find( - (usageYearFO) => usageYearFO.objectID === usageYear.id - ).refDirection, - })); + direction: refDirection, + }; } }