Skip to content

Commit

Permalink
feat(api): Search projects with maximum values
Browse files Browse the repository at this point in the history
  • Loading branch information
alepefe committed Dec 16, 2024
1 parent fcd33aa commit 24b5d9b
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 13 deletions.
4 changes: 3 additions & 1 deletion api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export class ProjectsController {
@TsRestHandler(projectsContract.getProjects)
async getProjects(): ControllerResponse {
return tsRestHandler(projectsContract.getProjects, async ({ query }) => {
const data = await this.projectsService.findAllPaginated(query);
const data = query.withMaximums
? await this.projectsService.findAllProjectsWithMaximums(query)
: await this.projectsService.findAllPaginated(query);
return { body: data, status: HttpStatus.OK };
});
}
Expand Down
46 changes: 38 additions & 8 deletions api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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 { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
import { z } from 'zod';
import { getProjectsQuerySchema } from '@shared/contracts/projects.contract';
import { PaginatedProjectsWithMaximums } from '@shared/dtos/projects/projects.dto';

export type ProjectFetchSpecificacion = z.infer<typeof getProjectsQuerySchema>;

Expand All @@ -16,26 +17,49 @@ export class ProjectsService extends AppBaseService<
unknown
> {
constructor(
@InjectDataSource()
private readonly dataSource: DataSource,
@InjectRepository(Project)
public readonly projectRepository: Repository<Project>,
) {
super(projectRepository, 'project', 'projects');
}

async extendFindAllQuery(
public async findAllProjectsWithMaximums(
query: ProjectFetchSpecificacion,
): Promise<PaginatedProjectsWithMaximums> {
const qb = this.dataSource
.createQueryBuilder()
.select('MAX(abatement_potential)::integer', 'maxAbatementPotential')
.addSelect('MAX(total_cost + total_cost_npv)::integer', 'maxTotalCost')
.from(Project, 'project');
const totalsQuery = this.applySearchFiltersToQueryBuilder(qb, query);

const [maximums, { metadata, data }] = await Promise.all([
totalsQuery.getRawOne(),
this.findAllPaginated(query),
]);
return {
metadata,
maximums,
data,
};
}

private applySearchFiltersToQueryBuilder(
query: SelectQueryBuilder<Project>,
fetchSpecification: ProjectFetchSpecificacion,
): Promise<SelectQueryBuilder<Project>> {
): SelectQueryBuilder<Project> {
// Filter by project name
if (fetchSpecification.partialProjectName) {
query = query.andWhere('project_name ILIKE :projectName', {
query.andWhere('project_name ILIKE :projectName', {
projectName: `%${fetchSpecification.partialProjectName}%`,
});
}

// Filter by abatement potential
if (fetchSpecification.abatementPotentialRange) {
query = query.andWhere(
query.andWhere(
'abatement_potential >= :minAP AND abatement_potential <= :maxAP',
{
minAP: Math.min(...fetchSpecification.abatementPotentialRange),
Expand All @@ -57,15 +81,21 @@ export class ProjectsService extends AppBaseService<
break;
}

query = query.andWhere(
query.andWhere(
`${filteredCostColumn} >= :minCost AND ${filteredCostColumn} <= :maxCost`,
{
minCost: Math.min(...fetchSpecification.costRange),
maxCost: Math.max(...fetchSpecification.costRange),
},
);
}

return query;
}

async extendFindAllQuery(
query: SelectQueryBuilder<Project>,
fetchSpecification: ProjectFetchSpecificacion,
): Promise<SelectQueryBuilder<Project>> {
return this.applySearchFiltersToQueryBuilder(query, fetchSpecification);
}
}
32 changes: 32 additions & 0 deletions api/test/integration/projects/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,38 @@ describe('Projects', () => {
});
});

test('Should return a list of filtered projects with maximum values', async () => {
await testManager.mocks().createProject({
id: 'e934e9fe-a79c-40a5-8254-8817851764ad',
projectName: 'PROJ_ABC',
totalCost: 100,
totalCostNPV: 50,
abatementPotential: 10,
});
await testManager.mocks().createProject({
id: 'e934e9fe-a79c-40a5-8254-8817851764ae',
projectName: 'PROJ_DEF',
totalCost: 200,
totalCostNPV: 100,
abatementPotential: 20,
});

const response = await testManager
.request()
.get(projectsContract.getProjects.path)
.query({
withMaximums: true,
partialProjectName: 'PROJ',
});

expect(response.status).toBe(HttpStatus.OK);
expect(response.body.data).toHaveLength(2);
expect(response.body.maximums).toEqual({
maxAbatementPotential: 20,
maxTotalCost: 300,
});
});

describe('Filters for Projects', () => {
test('Should get a list of countries there are projects in', async () => {
const fiveCountriesWithNoGeometry = countriesInDb
Expand Down
12 changes: 8 additions & 4 deletions shared/contracts/projects.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,36 @@ import { ProjectMap } from "@shared/dtos/projects/projects-map.dto";
import { generateEntityQuerySchema } from "@shared/schemas/query-param.schema";
import { BaseEntity } from "typeorm";
import { ProjectScorecardView } from "@shared/entities/project-scorecard.view";
import { PaginatedProjectsWithMaximums } from "@shared/dtos/projects/projects.dto";

const contract = initContract();

export type ProjectType = Omit<Project, keyof BaseEntity>;
export type ProjectScorecardType = Omit<ProjectScorecard, keyof BaseEntity>;

export const otherFilters = z.object({
export const otherParams = z.object({
costRange: z.coerce.number().array().optional(),
abatementPotentialRange: z.coerce.number().array().optional(),
costRangeSelector: z.nativeEnum(COST_TYPE_SELECTOR).optional(),
partialProjectName: z.string().optional(),
withMaximums: z.coerce.boolean().optional(),
});
export const projectsQuerySchema = generateEntityQuerySchema(Project);
export const projectScorecardQuerySchema =
generateEntityQuerySchema(ProjectScorecard);

export const getProjectsQuerySchema = projectsQuerySchema.merge(otherFilters);
export const getProjectsQuerySchema = projectsQuerySchema.merge(otherParams);
export const getProjectScorecardQuerySchema =
projectScorecardQuerySchema.merge(otherFilters);
projectScorecardQuerySchema.merge(otherParams);

export const projectsContract = contract.router({
getProjects: {
method: "GET",
path: "/projects",
responses: {
200: contract.type<ApiPaginationResponse<Project>>(),
200: contract.type<
ApiPaginationResponse<Project> | PaginatedProjectsWithMaximums
>(),
},
query: getProjectsQuerySchema,
},
Expand Down
11 changes: 11 additions & 0 deletions shared/dtos/projects/projects.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PaginationMeta } from "@shared/dtos/global/api-response.dto";
import { Project } from "@shared/entities/projects.entity";

export class PaginatedProjectsWithMaximums {
metadata: PaginationMeta;
data: Partial<Project>[];
maximums: {
maxAbatementPotential: number;
maxCost: number;
};
}

0 comments on commit 24b5d9b

Please sign in to comment.