From 3aa44b1d4f98870bea4ae06479df3d0020223490 Mon Sep 17 00:00:00 2001 From: manelcecs Date: Mon, 30 Oct 2023 09:46:17 +0100 Subject: [PATCH] Split individual services Minor refactor --- .../categories/category-service.ts | 44 +++++ .../flows/flow-search-service.ts | 171 +++++++++++++++++ src/domain-services/flows/flow-service.ts | 180 ------------------ src/domain-services/flows/graphql/resolver.ts | 14 +- src/domain-services/flows/graphql/types.ts | 66 +------ .../organizations/organization-service.ts | 37 ++-- 6 files changed, 247 insertions(+), 265 deletions(-) create mode 100644 src/domain-services/categories/category-service.ts create mode 100644 src/domain-services/flows/flow-search-service.ts delete mode 100644 src/domain-services/flows/flow-service.ts diff --git a/src/domain-services/categories/category-service.ts b/src/domain-services/categories/category-service.ts new file mode 100644 index 00000000..76f7307d --- /dev/null +++ b/src/domain-services/categories/category-service.ts @@ -0,0 +1,44 @@ +import { Database } from '@unocha/hpc-api-core/src/db'; +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'; + +@Service() +export class CategoryService { + async getFlowCategory(flow: any, models: Database): Promise { + const flowIdBranded = createBrandedValue(flow.id); + const flowLinks = await models.flowLink.find({ + where: { + childID: flowIdBranded, + }, + }); + + const flowLinksBrandedIds = flowLinks.map((flowLink) => + createBrandedValue(flowLink.parentID) + ); + + const categoriesRef = await models.categoryRef.find({ + where: { + objectID: { + [Op.IN]: flowLinksBrandedIds, + }, + versionID: flow.versionID, + }, + }); + + const categories = await models.category.find({ + where: { + id: { + [Op.IN]: categoriesRef.map((catRef) => catRef.categoryID), + }, + }, + }); + + return categories.map((cat) => ({ + id: cat.id, + name: cat.name, + group: cat.group, + })); + } +} diff --git a/src/domain-services/flows/flow-search-service.ts b/src/domain-services/flows/flow-search-service.ts new file mode 100644 index 00000000..c48d3ade --- /dev/null +++ b/src/domain-services/flows/flow-search-service.ts @@ -0,0 +1,171 @@ +import { Service } from 'typedi'; +import { + FlowCategory, + FlowLocation, + FlowOrganization, + FlowPlan, + FlowSearchResult, + FlowSortField, + FlowUsageYear, +} 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 { prepareConditionFromCursor } from '../../utils/graphql/pagination'; + +@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 + ) {} + + async search( + models: Database, + first: number, + afterCursor?: number, + beforeCursor?: number, + sortField?: FlowSortField, + sortOrder?: 'asc' | 'desc' + ): Promise { + if (beforeCursor && afterCursor) { + throw new Error('Cannot use before and after cursor at the same time'); + } + + const sortCondition = { + column: sortField ?? 'id', + 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 + ); + + flows = await models.flow.find({ + orderBy: sortCondition, + limit: first, + where: { + ...condition, + }, + }); + } else { + flows = await models.flow.find({ + orderBy: sortCondition, + limit: first, + }); + } + + 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 = + this.organizationService.getFlowObjectOrganizations( + organizationsFO, + models + ); + + const locationsPromise: Promise = + this.locationService.getFlowObjectLocations(locationsFO, models); + + const plansPromise: Promise = + this.planService.getFlowObjectPlans(plansFO, models); + + const usageYearsPromise: Promise = + 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(), + }; + }) + ); + + return { + items, + hasNextPage: first <= 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( + flow: any, + models: Database, + organizationsFO: any[], + locationsFO: any[], + plansFO: any[], + usageYearsFO: any[] + ): Promise { + const flowIdBranded = createBrandedValue(flow.id); + const flowObjects = await models.flowObject.find({ + where: { + flowID: flowIdBranded, + }, + }); + + 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); + } + }); + } +} diff --git a/src/domain-services/flows/flow-service.ts b/src/domain-services/flows/flow-service.ts deleted file mode 100644 index 0a63abe2..00000000 --- a/src/domain-services/flows/flow-service.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { Service } from 'typedi'; -import { - FlowCategory, - FlowLocation, - FlowOrganization, - FlowPlan, - FlowSearchResult, - FlowSortField, - FlowUsageYear, -} from './graphql/types'; -import { Database } from '@unocha/hpc-api-core/src/db/type'; -import { createBrandedValue } from '@unocha/hpc-api-core/src/util/types'; -import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; -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'; - -@Service() -export class FlowService { - constructor(private readonly organizationService: OrganizationService, - private readonly locationService: LocationService, - private readonly planService: PlanService, - private readonly usageYearService: UsageYearService) {} - - async search( - models: Database, - first: number, - afterCursor: string, - sortField: FlowSortField, - sortOrder: 'asc' | 'desc' - ): Promise { - let afterIndex = 0; - - const sortCondition = { - column: sortField ?? 'id', - order: sortOrder ?? 'DESC', - }; - - let flows = await models.flow.find({ - orderBy: sortCondition, - limit: first, - }); - const countRes = await models.flow.count(); - const count = countRes[0] as { count: number }; - - if (afterCursor) { - const after = flows.findIndex( - (flow) => flow.id.toString() === afterCursor - ); - if (after < 0) { - throw new Error('Cursor not found'); - } - afterIndex = after + 1; - } - - const pagedData = flows.slice(afterIndex, afterIndex + first); - const edges = await Promise.all( - pagedData.map(async (flow) => { - const categories: FlowCategory[] = await this.getFlowCategories( - flow, - models - ); - - const flowObjects = await this.getFlowObjects(flow, models); - - const organizationsFO: any[] = []; - const locationsFO: any[] = []; - const plansFO: any[] = []; - const usageYearsFO: any[] = []; - - 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); - } - }); - - const organizations: FlowOrganization[] = await this.organizationService.getFlowObjectOrganizations( - organizationsFO, - models - ); - - const locations: FlowLocation[] = await this.locationService.getFlowObjectLocations( - locationsFO, - models - ); - - const plans = await this.planService.getFlowObjectPlans(plansFO, models); - - const usageYears = await this.usageYearService.getFlowObjectUsageYears(usageYearsFO, models); - - return { - node: { - id: flow.id.valueOf(), - amountUSD: flow.amountUSD.toString(), - createdAt: flow.createdAt, - categories: categories, - organizations: organizations, - locations: locations, - plans: plans, - usageYears: usageYears, - }, - cursor: flow.id.toString(), - }; - }) - ); - - return { - edges, - pageInfo: { - hasNextPage: count.count > afterIndex, - hasPreviousPage: afterIndex > 0, - startCursor: pagedData.length ? pagedData[0].id.toString() : '', - endCursor: pagedData.length - ? pagedData[pagedData.length - 1].id.toString() - : '', - pageSize: pagedData.length, - sortField: sortCondition.column, - sortOrder: sortCondition.order, - }, - totalCount: count.count, - }; - } - - private async getFlowCategories( - flow: any, - models: Database - ): Promise { - const flowIdBranded = createBrandedValue(flow.id); - const flowLinks = await models.flowLink.find({ - where: { - childID: flowIdBranded, - }, - }); - - const flowLinksBrandedIds = flowLinks.map((flowLink) => - createBrandedValue(flowLink.parentID) - ); - - const categoriesRef = await models.categoryRef.find({ - where: { - objectID: { - [Op.IN]: flowLinksBrandedIds, - }, - versionID: flow.versionID, - }, - }); - - const categories = await models.category.find({ - where: { - id: { - [Op.IN]: categoriesRef.map((catRef) => catRef.categoryID), - }, - }, - }); - - return categories.map((cat) => ({ - id: cat.id, - name: cat.name, - group: cat.group, - })); - } - - private async getFlowObjects(flow: any, models: Database): Promise { - const flowIdBranded = createBrandedValue(flow.id); - const flowObjects = await models.flowObject.find({ - where: { - flowID: flowIdBranded, - }, - }); - - return flowObjects; - } -} diff --git a/src/domain-services/flows/graphql/resolver.ts b/src/domain-services/flows/graphql/resolver.ts index baebb962..b78c4393 100644 --- a/src/domain-services/flows/graphql/resolver.ts +++ b/src/domain-services/flows/graphql/resolver.ts @@ -1,19 +1,20 @@ -import Flow, { FlowSearchResult, FlowSortField } from './types'; +import Flow, { FlowSearchResult } from './types'; import { Service } from 'typedi'; -import { Arg, Args, Ctx, Query, Resolver } from 'type-graphql'; -import { FlowService } from '../flow-service'; +import { Arg, Ctx, Query, Resolver } from 'type-graphql'; +import { FlowSearchService } from '../flow-search-service'; import Context from '../../Context'; @Service() @Resolver(Flow) export default class FlowResolver { - constructor(private flowService: FlowService) {} + constructor(private flowSearchService: FlowSearchService) {} @Query(() => FlowSearchResult) async searchFlows( @Ctx() context: Context, @Arg('first', { nullable: false }) first: number, - @Arg('afterCursor', { nullable: true }) afterCursor: string, + @Arg('afterCursor', { nullable: true }) afterCursor: number, + @Arg('beforeCursor', { nullable: true }) beforeCursor: number, @Arg('sortField', { nullable: true }) sortField: | 'id' @@ -38,10 +39,11 @@ export default class FlowResolver { | 'deletedAt', @Arg('sortOrder', { nullable: true }) sortOrder: 'asc' | 'desc' ): Promise { - return await this.flowService.search( + return await this.flowSearchService.search( context.models, first, afterCursor, + beforeCursor, sortField, sortOrder ); diff --git a/src/domain-services/flows/graphql/types.ts b/src/domain-services/flows/graphql/types.ts index f8864e51..5efe1be2 100644 --- a/src/domain-services/flows/graphql/types.ts +++ b/src/domain-services/flows/graphql/types.ts @@ -1,4 +1,5 @@ import { Field, ObjectType } from 'type-graphql'; +import { ItemPaged, PageInfo } from '../../../utils/graphql/pagination'; @ObjectType() export class FlowCategory { @@ -52,7 +53,7 @@ export class FlowUsageYear { } @ObjectType() -export default class Flow { +export default class Flow implements ItemPaged { @Field({ nullable: false }) id: number; @@ -76,70 +77,15 @@ export default class Flow { @Field(() => [FlowUsageYear], { nullable: false }) usageYears: FlowUsageYear[]; -} - -@ObjectType() -export class FlowEdge { - @Field({ nullable: false }) - node: Flow; @Field({ nullable: false }) - cursor: string; + cursor: number; } @ObjectType() -export class PageInfo { - @Field({ nullable: false }) - hasNextPage: boolean; - - @Field({ nullable: false }) - hasPreviousPage: boolean; - - @Field({ nullable: false }) - startCursor: string; - - @Field({ nullable: false }) - endCursor: string; - - @Field({ nullable: false }) - pageSize: number; - - @Field({ nullable: false }) - sortField: - | 'id' - | 'amountUSD' - | 'versionID' - | 'activeStatus' - | 'restricted' - | 'newMoney' - | 'flowDate' - | 'decisionDate' - | 'firstReportedDate' - | 'budgetYear' - | 'origAmount' - | 'origCurrency' - | 'exchangeRate' - | 'description' - | 'notes' - | 'versionStartDate' - | 'versionEndDate' - | 'createdAt' - | 'updatedAt' - | 'deletedAt'; - - @Field({ nullable: false }) - sortOrder: string; -} -@ObjectType() -export class FlowSearchResult { - @Field(() => [FlowEdge], { nullable: false }) - edges: FlowEdge[]; - - @Field(() => PageInfo, { nullable: false }) - pageInfo: PageInfo; - - @Field({ nullable: false }) - totalCount: number; +export class FlowSearchResult extends PageInfo { + @Field(() => [Flow], { nullable: false }) + items: Flow[]; } export type FlowSortField = diff --git a/src/domain-services/organizations/organization-service.ts b/src/domain-services/organizations/organization-service.ts index 36e5a525..66805bb8 100644 --- a/src/domain-services/organizations/organization-service.ts +++ b/src/domain-services/organizations/organization-service.ts @@ -1,24 +1,23 @@ -import { Database } from "@unocha/hpc-api-core/src/db"; -import { Service } from "typedi"; +import { Database } from '@unocha/hpc-api-core/src/db'; +import { Service } from 'typedi'; import { Op } from '@unocha/hpc-api-core/src/db/util/conditions'; @Service() export class OrganizationService { + async getFlowObjectOrganizations(organizationsFO: any[], models: Database) { + const organizations = await models.organization.find({ + where: { + id: { + [Op.IN]: organizationsFO.map((orgFO) => orgFO.objectID), + }, + }, + }); - async getFlowObjectOrganizations(organizationsFO: any[], models: Database){ - const organizations = await models.organization.find({ - where: { - id: { - [Op.IN]: organizationsFO.map((orgFO) => orgFO.objectID), - }, - }, - }); - - return organizations.map((org) => ({ - id: org.id, - refDirection: organizationsFO.find((orgFO) => orgFO.objectID === org.id) - .refDirection, - name: org.name, - })); - } -} \ No newline at end of file + return organizations.map((org) => ({ + id: org.id, + refDirection: organizationsFO.find((orgFO) => orgFO.objectID === org.id) + .refDirection, + name: org.name, + })); + } +}