Skip to content

Commit

Permalink
modified open api docs with agency budget param for cap projects, reg…
Browse files Browse the repository at this point in the history
…en ts definitions and zod schemas

added agency budget filter to findMany in repo, error in service, cleanup in agency budget e2e test
repo and service completed
findMany mock, e2e test, service tests
service spec for filter by agency and missing agency code
new gen files
  • Loading branch information
horatiorosa committed Feb 12, 2025
1 parent 745541d commit 9af1b51
Show file tree
Hide file tree
Showing 15 changed files with 1,691 additions and 3,081 deletions.
8 changes: 8 additions & 0 deletions openapi/components/parameters/agencyBudgetParam.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: agencyBudget
required: false
in: query
schema:
type: string
example: 'HR'
description: >-
The two character alphabetic string containing the letters used to refer to the agency budget code.
1 change: 1 addition & 0 deletions openapi/paths/capital-projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ get:
parameters:
- $ref: ../components/parameters/communityDistrictIdQueryParam.yaml
- $ref: ../components/parameters/cityCouncilDistrictIdQueryParam.yaml
- $ref: ../components/parameters/agencyBudgetParam.yaml
- $ref: ../components/parameters/limitParam.yaml
- $ref: ../components/parameters/offsetParam.yaml
responses:
Expand Down
4,599 changes: 1,520 additions & 3,079 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/agency-budget/agency-budget.repository.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ import { z } from "zod";
export const findManyRepoSchema = z.array(agencyBudgetEntitySchema);

export type FindManyRepo = z.infer<typeof findManyRepoSchema>;

export const checkByCodeRepoSchema = agencyBudgetEntitySchema.pick({
code: true,
});

export type CheckByCodeRepo = z.infer<typeof checkByCodeRepoSchema>;
21 changes: 20 additions & 1 deletion src/agency-budget/agency-budget.repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Inject } from "@nestjs/common";
import { DataRetrievalException } from "src/exception";
import { DB, DbType } from "src/global/providers/db.provider";
import { FindManyRepo } from "./agency-budget.repository.schema";
import {
CheckByCodeRepo,
FindManyRepo,
} from "./agency-budget.repository.schema";
import { agencyBudget } from "src/schema";

export class AgencyBudgetRepository {
Expand All @@ -10,6 +13,22 @@ export class AgencyBudgetRepository {
private readonly db: DbType,
) {}

#checkByCode = this.db.query.agencyBudget
.findFirst({
columns: { code: true },
where: (agencyBudget, { eq, sql }) =>
eq(agencyBudget.code, sql.placeholder("code ")),
})
.prepare("checkByCode");

async checkByCode(code: string): Promise<CheckByCodeRepo | undefined> {
try {
return await this.#checkByCode.execute({ code });
} catch {
throw new DataRetrievalException();
}
}

async findMany(): Promise<FindManyRepo> {
try {
return await this.db.query.agencyBudget.findMany({
Expand Down
1 change: 1 addition & 0 deletions src/capital-project/capital-project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class CapitalProjectController {
offset: queryParams.offset,
cityCouncilDistrictId: queryParams.cityCouncilDistrictId,
communityDistrictCombinedId: queryParams.communityDistrictId,
agencyBudget: queryParams.agencyBudget,
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/capital-project/capital-project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CapitalProjectService } from "./capital-project.service";
import { CapitalProjectRepository } from "./capital-project.repository";
import { CityCouncilDistrictRepository } from "src/city-council-district/city-council-district.repository";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";
import { AgencyBudgetRepository } from "../agency-budget/agency-budget.repository";

@Module({
exports: [CapitalProjectService],
Expand All @@ -12,6 +13,7 @@ import { CommunityDistrictRepository } from "src/community-district/community-di
CapitalProjectRepository,
CityCouncilDistrictRepository,
CommunityDistrictRepository,
AgencyBudgetRepository,
],
controllers: [CapitalProjectController],
})
Expand Down
12 changes: 12 additions & 0 deletions src/capital-project/capital-project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ export class CapitalProjectRepository {
cityCouncilDistrictId,
communityDistrictId,
boroughId,
agencyBudget,
limit,
offset,
}: {
cityCouncilDistrictId: string | null;
communityDistrictId: string | null;
boroughId: string | null;
agencyBudget: string | null;
limit: number;
offset: number;
}): Promise<FindManyRepo> {
Expand Down Expand Up @@ -70,6 +72,13 @@ export class CapitalProjectRepository {
ST_Intersects(${communityDistrict.liFt}, ${capitalProject.liFtMPoly})
OR ST_Intersects(${communityDistrict.liFt}, ${capitalProject.liFtMPnt})`,
)
.leftJoin(
capitalCommitment,
and(
eq(capitalProject.managingCode, capitalCommitment.managingCode),
eq(capitalProject.id, capitalCommitment.capitalProjectId),
),
)
.where(
and(
cityCouncilDistrictId !== null
Expand All @@ -81,6 +90,9 @@ export class CapitalProjectRepository {
eq(communityDistrict.id, communityDistrictId),
)
: undefined,
agencyBudget !== null
? eq(capitalCommitment.budgetLineCode, agencyBudget)
: undefined,
),
)
.limit(limit)
Expand Down
37 changes: 37 additions & 0 deletions src/capital-project/capital-project.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CapitalProjectRepositoryMock } from "test/capital-project/capital-proje
import { CityCouncilDistrictRepositoryMock } from "test/city-council-district/city-council-district.repository.mock";
import { CommunityDistrictRepositoryMock } from "test/community-district/community-district.repository.mock";
import { BoroughRepositoryMock } from "test/borough/borough.repository.mock";
import { AgencyBudgetRepositoryMock } from "test/agency-budget/agency-budget.repository.mock";
import { CapitalProjectService } from "./capital-project.service";
import { Test } from "@nestjs/testing";
import { CapitalProjectRepository } from "./capital-project.repository";
Expand All @@ -14,13 +15,15 @@ import {
findCapitalProjectTilesQueryResponseSchema,
findCapitalProjectsQueryResponseSchema,
} from "src/gen";
import { AgencyBudgetRepository } from "src/agency-budget/agency-budget.repository";
import {
InvalidRequestParameterException,
ResourceNotFoundException,
} from "src/exception";

describe("CapitalProjectService", () => {
let capitalProjectService: CapitalProjectService;
const agencyBudgetRepositoryMock = new AgencyBudgetRepositoryMock();

const cityCouncilDistrictRepositoryMock =
new CityCouncilDistrictRepositoryMock();
Expand All @@ -31,6 +34,7 @@ describe("CapitalProjectService", () => {
const capitalProjectRepository = new CapitalProjectRepositoryMock(
cityCouncilDistrictRepositoryMock,
communityDistrictRepositoryMock,
agencyBudgetRepositoryMock,
);

beforeEach(async () => {
Expand All @@ -40,6 +44,7 @@ describe("CapitalProjectService", () => {
CapitalProjectRepository,
CityCouncilDistrictRepository,
CommunityDistrictRepository,
AgencyBudgetRepository,
],
})
.overrideProvider(CapitalProjectRepository)
Expand All @@ -48,6 +53,8 @@ describe("CapitalProjectService", () => {
.useValue(cityCouncilDistrictRepositoryMock)
.overrideProvider(CommunityDistrictRepository)
.useValue(communityDistrictRepositoryMock)
.overrideProvider(AgencyBudgetRepository)
.useValue(agencyBudgetRepositoryMock)
.compile();

capitalProjectService = moduleRef.get<CapitalProjectService>(
Expand All @@ -67,6 +74,7 @@ describe("CapitalProjectService", () => {
);
expect(parsedBody.limit).toBe(20);
expect(parsedBody.offset).toBe(0);
expect(parsedBody.capitalProjects.length).toBe(8);
expect(parsedBody.total).toBe(parsedBody.capitalProjects.length);
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});
Expand Down Expand Up @@ -122,6 +130,25 @@ describe("CapitalProjectService", () => {
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});

it("should filter by an agency budget code", async () => {
const agencyBudget =
capitalProjectRepository.agencyBudgetRepositoryMock.checkByCodeMocks[1]
.code;
const capitalProjectsResponse = await capitalProjectService.findMany({
agencyBudget: agencyBudget,
});
expect(() =>
findCapitalProjectsQueryResponseSchema.parse(capitalProjectsResponse),
).not.toThrow();

const parsedBody = findCapitalProjectsQueryResponseSchema.parse(
capitalProjectsResponse,
);
expect(parsedBody.capitalProjects.length).toBe(7);
expect(parsedBody.total).toBe(parsedBody.capitalProjects.length);
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});

it("should return a InvalidRequestParameterException error when a community district with the given id cannot be found", async () => {
const id = "999";

Expand All @@ -131,6 +158,16 @@ describe("CapitalProjectService", () => {
}),
).rejects.toThrow(InvalidRequestParameterException);
});

it("should throw an error when requesting an agency budget that does not exist", async () => {
const missingAgencyBudget = "hr";

expect(() =>
capitalProjectService.findMany({
agencyBudget: missingAgencyBudget,
}),
).rejects.toThrow(InvalidRequestParameterException);
});
});

describe("findByManagingCodeCapitalProjectId", () => {
Expand Down
10 changes: 10 additions & 0 deletions src/capital-project/capital-project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ import {
} from "./capital-project.repository.schema";
import { CityCouncilDistrictRepository } from "src/city-council-district/city-council-district.repository";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";
import { AgencyBudgetRepository } from "src/agency-budget/agency-budget.repository";

export class CapitalProjectService {
constructor(
@Inject(CapitalProjectRepository)
private readonly capitalProjectRepository: CapitalProjectRepository,
private readonly cityCouncilDistrictRepository: CityCouncilDistrictRepository,
private readonly communityDistrictRepository: CommunityDistrictRepository,
@Inject(AgencyBudgetRepository)
private readonly agencyBudgetRepository: AgencyBudgetRepository,
) {}

async findMany({
limit = 20,
offset = 0,
cityCouncilDistrictId = null,
communityDistrictCombinedId = null,
agencyBudget = null,
}: {
limit?: number;
offset?: number;
cityCouncilDistrictId?: string | null;
communityDistrictCombinedId?: string | null;
agencyBudget?: string | null;
}) {
const checklist: Array<Promise<unknown | undefined>> = [];
if (cityCouncilDistrictId !== null)
Expand All @@ -61,6 +66,10 @@ export class CapitalProjectService {
communityDistrictId,
),
);

if (agencyBudget !== null) {
checklist.push(this.agencyBudgetRepository.checkByCode(agencyBudget));
}
const checkedList = await Promise.all(checklist);
if (checkedList.some((result) => result === undefined))
throw new InvalidRequestParameterException();
Expand All @@ -69,6 +78,7 @@ export class CapitalProjectService {
cityCouncilDistrictId,
boroughId,
communityDistrictId,
agencyBudget,
limit,
offset,
});
Expand Down
5 changes: 5 additions & 0 deletions src/gen/types/FindCapitalProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export type FindCapitalProjectsQueryParams = {
* @type string | undefined
*/
cityCouncilDistrictId?: string;
/**
* @description The two character alphabetic string containing the letters used to refer to the agency budget code.
* @type string | undefined
*/
agencyBudget?: string;
/**
* @description The maximum number of results to be returned in each response. The default value is 20. It must be between 1 and 100, inclusive.
* @type integer | undefined
Expand Down
6 changes: 6 additions & 0 deletions src/gen/zod/findCapitalProjectsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export const findCapitalProjectsQueryParamsSchema = z
"One or two character code to represent city council districts.",
)
.optional(),
agencyBudget: z.coerce
.string()
.describe(
"The two character alphabetic string containing the letters used to refer to the agency budget code.",
)
.optional(),
limit: z.coerce
.number()
.int()
Expand Down
15 changes: 14 additions & 1 deletion test/agency-budget/agency-budget.repository.mock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { generateMock } from "@anatine/zod-mock";
import { findManyRepoSchema } from "src/agency-budget/agency-budget.repository.schema";
import {
checkByCodeRepoSchema,
findManyRepoSchema,
} from "src/agency-budget/agency-budget.repository.schema";

export class AgencyBudgetRepositoryMock {
numberOfMocks = 2;

checkByCodeMocks = Array.from(Array(this.numberOfMocks), (_, seed) =>
generateMock(checkByCodeRepoSchema, { seed: seed + 1 }),
);

async checkByCode(code: string) {
return this.checkByCodeMocks.find((row) => row.code === code);
}

findManyMocks = generateMock(findManyRepoSchema);

async findMany() {
Expand Down
35 changes: 35 additions & 0 deletions test/capital-project/capital-project.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CityCouncilDistrictRepository } from "src/city-council-district/city-co
import { CityCouncilDistrictRepositoryMock } from "test/city-council-district/city-council-district.repository.mock";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";
import { CommunityDistrictRepositoryMock } from "test/community-district/community-district.repository.mock";
import { AgencyBudgetRepositoryMock } from "test/agency-budget/agency-budget.repository.mock";
import * as request from "supertest";
import { HttpName } from "src/filter";
import {
Expand All @@ -19,16 +20,20 @@ import {
findCapitalProjectGeoJsonByManagingCodeCapitalProjectIdQueryResponseSchema,
findCapitalProjectsQueryResponseSchema,
} from "src/gen";
import { AgencyBudgetRepository } from "src/agency-budget/agency-budget.repository";

describe("Capital Projects", () => {
let app: INestApplication;

const cityCouncilDistrictRepository = new CityCouncilDistrictRepositoryMock();
const communityDistrictRepository = new CommunityDistrictRepositoryMock();
const agencyBudgetRepositoryMock = new AgencyBudgetRepositoryMock();
const capitalProjectRepository = new CapitalProjectRepositoryMock(
cityCouncilDistrictRepository,
communityDistrictRepository,
agencyBudgetRepositoryMock,
);

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CapitalProjectModule],
Expand All @@ -39,6 +44,8 @@ describe("Capital Projects", () => {
.useValue(cityCouncilDistrictRepository)
.overrideProvider(CommunityDistrictRepository)
.useValue(communityDistrictRepository)
.overrideProvider(AgencyBudgetRepository)
.useValue(agencyBudgetRepositoryMock)
.compile();

app = moduleRef.createNestApplication();
Expand Down Expand Up @@ -85,6 +92,34 @@ describe("Capital Projects", () => {
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});

it("should 200 and return capital projects with page metadata when specifying a valid agency budget code", async () => {
const agencyBudget =
capitalProjectRepository.agencyBudgetRepositoryMock.checkByCodeMocks[0];
const response = await request(app.getHttpServer()).get(
`/capital-projects?agencyBudget=${agencyBudget.code}`,
);

expect(() =>
findCapitalProjectsQueryResponseSchema.parse(response.body),
).not.toThrow();
const parsedBody = findCapitalProjectsQueryResponseSchema.parse(
response.body,
);
expect(parsedBody.total).toBe(1);
});

it("should 400 when finding by an agency budget code that does not exist", async () => {
const agencyBudgetCode = "DNE";
const response = await request(app.getHttpServer()).get(
`/capital-projects?agencyBudget=${agencyBudgetCode}`,
);

expect(response.body.message).toBe(
new InvalidRequestParameterException().message,
);
expect(response.body.error).toBe(HttpName.BAD_REQUEST);
});

it("should 400 when finding by an invalid limit", async () => {
const response = await request(app.getHttpServer()).get(
"/capital-projects?limit=b4d",
Expand Down
Loading

0 comments on commit 9af1b51

Please sign in to comment.