Skip to content

Commit

Permalink
Add filters
Browse files Browse the repository at this point in the history
  • Loading branch information
catalin-oancea committed Nov 5, 2024
1 parent c42423b commit 18dc8d0
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 3 deletions.
14 changes: 13 additions & 1 deletion api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@ export class ProjectsController {
@TsRestHandler(projectsContract.getProjects)
async getProjects(): ControllerResponse {
return tsRestHandler(projectsContract.getProjects, async ({ query }) => {
const data = await this.projectsService.findAllPaginated(query);
// The following filters do not work out of the box with the BaseService implementation
const otherFilters = {
costRange: query.filter?.costRange,
abatementPotentialRange: query.filter?.abatementPotentialRange,
costRangeSelector: query.filter?.costRangeSelector,
};
delete query.filter?.costRange;
delete query.filter?.abatementPotentialRange;
delete query.filter?.costRangeSelector;

const data = await this.projectsService.findAllPaginated(query, {
otherFilters: otherFilters,
});
return { body: data, status: HttpStatus.OK };
});
}
Expand Down
68 changes: 66 additions & 2 deletions api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { FetchSpecification } from 'nestjs-base-service';
import { Injectable } from '@nestjs/common';
import { AppBaseService } from '@api/utils/app-base.service';
import { Project } from '@shared/entities/projects.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, SelectQueryBuilder } from 'typeorm';
import { FetchSpecification } from 'nestjs-base-service';

interface ExtendedFetchSpecification extends FetchSpecification {
otherFilters?: {
abatementPotentialRange?: number[];
costRange?: number[];
costRangeSelector?: 'npv' | 'total';
};
}

@Injectable()
export class ProjectsService extends AppBaseService<
Expand All @@ -21,14 +29,70 @@ export class ProjectsService extends AppBaseService<

async extendFindAllQuery(
query: SelectQueryBuilder<Project>,
fetchSpecification: FetchSpecification,
fetchSpecification: ExtendedFetchSpecification,
): Promise<SelectQueryBuilder<Project>> {
// Filter by project name
if (fetchSpecification?.filter?.projectName) {
query = query.andWhere('project_name ILIKE :projectName', {
projectName: `%${fetchSpecification.filter.projectName}%`,
});
}

// Filter by abatement potential
if (this.isAbatementPotentialFilterValid(fetchSpecification)) {
query = query.andWhere(
'abatement_potential >= :minAP AND abatement_potential <= :maxAP',
{
minAP: fetchSpecification.otherFilters.abatementPotentialRange[0],
maxAP: fetchSpecification.otherFilters.abatementPotentialRange[1],
},
);
}

// Filter by cost (total or NPV)
if (this.isCostFilterValid(fetchSpecification)) {
let filteredCostColumn: string;
switch (fetchSpecification.otherFilters.costRangeSelector) {
case 'npv':
filteredCostColumn = 'total_cost_npv';
break;
case 'total':
default:
filteredCostColumn = 'total_cost';
break;
}

query = query.andWhere(
`${filteredCostColumn} >= :minCost AND ${filteredCostColumn} <= :maxCost`,
{
minCost: fetchSpecification.otherFilters.costRange[0],
maxCost: fetchSpecification.otherFilters.costRange[1],
},
);
}
return query;
}

private isCostFilterValid(
fetchSpecification: ExtendedFetchSpecification,
): boolean {
return !!(
fetchSpecification?.otherFilters?.costRange &&
fetchSpecification?.otherFilters?.costRange.length === 2 &&
fetchSpecification?.otherFilters?.costRange[0] <=
fetchSpecification?.otherFilters?.costRange[1] &&
fetchSpecification?.otherFilters?.costRangeSelector
);
}

private isAbatementPotentialFilterValid(
fetchSpecification: ExtendedFetchSpecification,
): boolean {
return !!(
fetchSpecification?.otherFilters?.abatementPotentialRange &&
fetchSpecification?.otherFilters?.abatementPotentialRange.length === 2 &&
fetchSpecification?.otherFilters?.abatementPotentialRange[0] <=
fetchSpecification?.otherFilters?.abatementPotentialRange[1]
);
}
}
75 changes: 75 additions & 0 deletions api/test/integration/projects/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,81 @@ describe('Projects', () => {
);
});

test('Should return a list of projects filtered by min/max NPV cost', async () => {
const projects: Project[] = [];
projects.push(
await testManager.mocks().createProject({ totalCostNPV: 25 }),
await testManager.mocks().createProject({ totalCostNPV: 15 }),
await testManager.mocks().createProject({ totalCostNPV: 45 }),
await testManager.mocks().createProject({ totalCostNPV: 10 }),
);

const response = await testManager
.request()
.get(projectsContract.getProjects.path)
.query({
filter: {
costRangeSelector: 'npv',
costRange: [12, 26],
},
});
expect(response.body.data).toHaveLength(2);
expect(
response.body.data
.map((project: Project) => project.totalCostNPV)
.sort(),
).toEqual(['15', '25']);
});

test('Should return a list of projects filtered by min/max total cost', async () => {
const projects: Project[] = [];
projects.push(
await testManager.mocks().createProject({ totalCost: 25 }),
await testManager.mocks().createProject({ totalCost: 15 }),
await testManager.mocks().createProject({ totalCost: 45 }),
await testManager.mocks().createProject({ totalCost: 10 }),
);

const response = await testManager
.request()
.get(projectsContract.getProjects.path)
.query({
filter: {
costRangeSelector: 'total',
costRange: [12, 26],
},
});
expect(response.body.data).toHaveLength(2);
expect(
response.body.data.map((project: Project) => project.totalCost).sort(),
).toEqual(['15', '25']);
});

test('Should return a list of projects filtered by min/max abatement potential', async () => {
const projects: Project[] = [];
projects.push(
await testManager.mocks().createProject({ abatementPotential: 25 }),
await testManager.mocks().createProject({ abatementPotential: 15 }),
await testManager.mocks().createProject({ abatementPotential: 45 }),
await testManager.mocks().createProject({ abatementPotential: 10 }),
);

const response = await testManager
.request()
.get(projectsContract.getProjects.path)
.query({
filter: {
abatementPotentialRange: [12, 26],
},
});
expect(response.body.data).toHaveLength(2);
expect(
response.body.data
.map((project: Project) => project.abatementPotential)
.sort(),
).toEqual(['15', '25']);
});

test('Should return a list of projects filtered by project name', async () => {
const projects: Project[] = [];
projects.push(
Expand Down

0 comments on commit 18dc8d0

Please sign in to comment.