diff --git a/src/domain-services/flows/flow-link-service.ts b/src/domain-services/flows/flow-link-service.ts index 1b21e7fd..eac0e356 100644 --- a/src/domain-services/flows/flow-link-service.ts +++ b/src/domain-services/flows/flow-link-service.ts @@ -1,6 +1,7 @@ import { FlowId } from '@unocha/hpc-api-core/src/db/models/flow'; import { Database } from '@unocha/hpc-api-core/src/db/type'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; +import { InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types'; import { Service } from 'typedi'; @Service() @@ -8,7 +9,7 @@ export class FlowLinkService { async getFlowLinksForFlows( flowIds: FlowId[], models: Database - ): Promise> { + ): Promise[]>> { const flowLinks = await models.flowLink.find({ where: { childID: { @@ -18,7 +19,7 @@ export class FlowLinkService { }); // Group flowLinks by flow ID for easy mapping - const flowLinksMap = new Map(); + const flowLinksMap = new Map[]>(); // Populate the map with flowLinks for each flow flowLinks.forEach((flowLink) => { diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts index e548fbd3..808545ad 100644 --- a/src/domain-services/flows/flow-search-service.ts +++ b/src/domain-services/flows/flow-search-service.ts @@ -16,6 +16,12 @@ 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 { + FlowObjectFilters, + SearchFlowsArgs, + SearchFlowsFilters, +} from './graphql/args'; @Service() export class FlowSearchService { @@ -27,18 +33,16 @@ export class FlowSearchService { private readonly categoryService: CategoryService, private readonly flowLinkService: FlowLinkService, private readonly externalReferenceService: ExternalReferenceService, - private readonly reportDetailService: ReportDetailService + private readonly reportDetailService: ReportDetailService, + private readonly flowService: FlowService ) {} async search( models: Database, - limit: number = 50, - sortOrder: 'asc' | 'desc' = 'desc', - sortField: FlowSortField = 'id', - afterCursor?: number, - beforeCursor?: number, - filters?: any + filters: SearchFlowsArgs ): Promise { + const { limit, afterCursor, beforeCursor, sortField, sortOrder } = filters; + if (beforeCursor && afterCursor) { throw new Error('Cannot use before and after cursor at the same time'); } @@ -50,6 +54,28 @@ export class FlowSearchService { const limitComputed = limit + 1; // Fetch one more item to check for hasNextPage + const { flowFilters, flowObjectFilters } = filters; + + let onlyFlowFilters = false; + let onlyFlowObjectFilters = false; + let bothFlowFilters = false; + + if ( + (!flowFilters && !flowObjectFilters) || + (flowFilters && !flowObjectFilters) + ) { + onlyFlowFilters = true; + } else if (!flowFilters && flowObjectFilters) { + onlyFlowObjectFilters = true; + } else if (flowFilters && flowObjectFilters) { + bothFlowFilters = true; + } + + const { flowObjectConditions, flowConditions } = [ + this.prepareFlowObjectConditions(flowObjectFilters), + this.prepareFlowConditions(flowFilters), + ]; + let condition; if (afterCursor) { condition = { @@ -73,12 +99,13 @@ export class FlowSearchService { } const [flows, countRes] = await Promise.all([ - models.flow.find({ - orderBy: sortCondition, - limit: limitComputed, - where: condition, - }), - models.flow.count({ where: condition }), + this.flowService.getFlows( + models, + condition, + sortCondition, + limitComputed + ), + this.flowService.getFlowsCount(models, condition), ]); const hasNextPage = flows.length > limit; @@ -138,7 +165,7 @@ export class FlowSearchService { 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 locations = [...(locationsMap.get(flow.id) || [])]; const plans = plansMap.get(flow.id) || []; const usageYears = usageYearsMap.get(flow.id) || []; const externalReferences = externalReferencesMap.get(flow.id) || []; @@ -149,14 +176,19 @@ export class FlowSearchService { this.getParketParents(flow, flowLink, models, parkedParentSource); } - // TODO: change and use flow.depth to verify (depth > 0) const childIDs: number[] = flowLinksMap .get(flow.id) - ?.map((flowLink) => flowLink.childID.valueOf()) as number[]; + ?.filter( + (flowLink) => flowLink.parentID === flow.id && flowLink.depth > 0 + ) + .map((flowLink) => flowLink.childID.valueOf()) as number[]; const parentIDs: number[] = flowLinksMap .get(flow.id) - ?.map((flowLink) => flowLink.parentID.valueOf()) as number[]; + ?.filter( + (flowLink) => flowLink.childID === flow.id && flowLink.depth > 0 + ) + .map((flowLink) => flowLink.parentID.valueOf()) as number[]; return { // Mandatory fields @@ -268,4 +300,50 @@ export class FlowSearchService { ); }); } + + 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) { + flowConditions.set(key, value); + } + }); + } + + return flowConditions; + } + } diff --git a/src/domain-services/flows/flow-service.ts b/src/domain-services/flows/flow-service.ts new file mode 100644 index 00000000..536ea3d8 --- /dev/null +++ b/src/domain-services/flows/flow-service.ts @@ -0,0 +1,24 @@ +import { Service } from 'typedi'; +import { Database } from '@unocha/hpc-api-core/src/db/type'; + +@Service() +export class FlowService { + constructor() {} + + async getFlows( + models: Database, + conditions: any, + orderBy: any, + limit: number + ) { + return await models.flow.find({ + orderBy, + limit, + where: conditions, + }); + } + + async getFlowsCount(models: Database, conditions: any) { + return await models.flow.count({ where: conditions }); + } +} diff --git a/src/domain-services/flows/graphql/args.ts b/src/domain-services/flows/graphql/args.ts index d55b3137..f07ffb0c 100644 --- a/src/domain-services/flows/graphql/args.ts +++ b/src/domain-services/flows/graphql/args.ts @@ -4,12 +4,73 @@ import { PaginationArgs } from '../../../utils/graphql/pagination'; @InputType() export class SearchFlowsFilters { + @Field({ nullable: true }) + id: number; + @Field({ nullable: true }) activeStatus: boolean; + + @Field({ nullable: true }) + status: 'commitment' | 'paid' | 'pledged'; + + @Field({ nullable: true }) + type: 'carryover' | 'parked' | 'pass_through' | 'standard'; + + @Field({ nullable: true }) + amountUSD: number; + + @Field({ nullable: true }) + reporterReferenceCode: number; + + @Field({ nullable: true }) + sourceSystemId: number; + + @Field({ nullable: true }) + legacyId: number; +} + +@InputType() +export class FlowObjectFilters { + @Field({ nullable: false }) + objectID: number; + + @Field({ nullable: false }) + direction: 'source' | 'destination'; + + @Field({ nullable: false }) + objectType: + | 'location' + | 'organization' + | 'plan' + | 'usageYear' + | 'category' + | 'project' + | 'globalCluster' + | 'emergency'; +} + +@InputType() +export class FlowCategory{ + + @Field({ nullable: false }) + id: number; + + @Field({ nullable: false }) + group: string; + } @ArgsType() export class SearchFlowsArgs extends PaginationArgs { @Field(() => SearchFlowsFilters, { nullable: true }) - filters: SearchFlowsFilters; + flowFilters: SearchFlowsFilters; + + @Field(() => [FlowObjectFilters], { nullable: true }) + flowObjectFilters: FlowObjectFilters[]; + + @Field(() => [FlowCategory], { nullable: true }) + categoryFilters: FlowCategory[]; + + @Field({ nullable: true }) + includeChildrenOfParkedFlows: boolean; } diff --git a/src/domain-services/flows/graphql/resolver.ts b/src/domain-services/flows/graphql/resolver.ts index 394a8915..80a96a64 100644 --- a/src/domain-services/flows/graphql/resolver.ts +++ b/src/domain-services/flows/graphql/resolver.ts @@ -3,8 +3,7 @@ import { Service } from 'typedi'; import { Arg, Args, Ctx, Query, Resolver } from 'type-graphql'; import { FlowSearchService } from '../flow-search-service'; import Context from '../../Context'; -import { SearchFlowsFilters } from './args'; -import { PaginationArgs } from '../../../utils/graphql/pagination'; +import { SearchFlowsArgs } from './args'; @Service() @Resolver(FlowPaged) @@ -14,23 +13,9 @@ export default class FlowResolver { @Query(() => FlowSearchResult) async searchFlows( @Ctx() context: Context, - @Args(() => PaginationArgs, { validate: false }) - pagination: PaginationArgs, - @Arg('activeStatus', { nullable: true }) activeStatus: boolean + @Args(() => SearchFlowsArgs, { validate: false }) + args: SearchFlowsArgs ): Promise { - const { limit, sortOrder, sortField, afterCursor, beforeCursor } = - pagination; - const filters: SearchFlowsFilters = { - activeStatus, - }; - return await this.flowSearchService.search( - context.models, - limit, - sortOrder, - sortField, - afterCursor, - beforeCursor, - filters - ); + return await this.flowSearchService.search(context.models, args); } } diff --git a/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts b/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts new file mode 100644 index 00000000..ecac6afd --- /dev/null +++ b/src/domain-services/flows/strategy/only-flow-conditions-strategy.ts @@ -0,0 +1,18 @@ +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 diff --git a/src/domain-services/organizations/graphql/types.ts b/src/domain-services/organizations/graphql/types.ts index e4a52057..c4ebdb23 100644 --- a/src/domain-services/organizations/graphql/types.ts +++ b/src/domain-services/organizations/graphql/types.ts @@ -11,4 +11,7 @@ export class Organization extends BaseType { @Field({ nullable: true }) name: string; + + @Field({ nullable: true }) + abbreviation: string; } diff --git a/src/domain-services/organizations/organization-service.ts b/src/domain-services/organizations/organization-service.ts index 1bac9e1d..a35e6637 100644 --- a/src/domain-services/organizations/organization-service.ts +++ b/src/domain-services/organizations/organization-service.ts @@ -54,6 +54,7 @@ export class OrganizationService { name: organization.name, createdAt: organization.createdAt.toISOString(), updatedAt: organization.updatedAt.toISOString(), + abbreviation: organization.abbreviation, }; } }