Skip to content

Commit

Permalink
Refactor service to increase Query Pool using IN strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
manelcecs committed Nov 2, 2023
1 parent ae70c3d commit dd2aa2b
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 129 deletions.
49 changes: 40 additions & 9 deletions src/domain-services/categories/category-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
});

Expand All @@ -23,7 +29,6 @@ export class CategoryService {
objectID: {
[Op.IN]: flowLinksBrandedIds,
},
versionID: flow.versionID,
},
});

Expand All @@ -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,
});
}
192 changes: 100 additions & 92 deletions src/domain-services/flows/flow-search-service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
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';
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 {
Expand All @@ -29,7 +22,7 @@ export class FlowSearchService {

async search(
models: Database,
first: number,
limit: number,
afterCursor?: number,
beforeCursor?: number,
sortField?: FlowSortField,
Expand All @@ -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,
Expand All @@ -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,
},
},
});

Expand Down
4 changes: 2 additions & 2 deletions src/domain-services/flows/graphql/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -41,7 +41,7 @@ export default class FlowResolver {
): Promise<FlowSearchResult> {
return await this.flowSearchService.search(
context.models,
first,
limit,
afterCursor,
beforeCursor,
sortField,
Expand Down
31 changes: 25 additions & 6 deletions src/domain-services/location/location-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
};
}
}
Loading

0 comments on commit dd2aa2b

Please sign in to comment.