Skip to content

Commit

Permalink
Implement find capital projects by community district
Browse files Browse the repository at this point in the history
Flesh out repository

Generated zod schema can't be nullable

Remove WIP emoji

Comment out pipe to get capital projects

Add unit test

Add e2e test

Fix type errors

Add index

Clean up

Add pipe validation

Use transform pipe in param and query decorator

Add tests for query params

Move check community district id to community district domain

Fix unit tests

Get community district mock for borough repository mock

Use dependency injection to inject community district mock into borough mock

Use dependency injection to inject community district mock into borough mock

Expand unit test
  • Loading branch information
pratishta committed Jul 5, 2024
1 parent 8fa9ab7 commit a796442
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 12 deletions.
8 changes: 4 additions & 4 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ paths:
$ref: "#/components/responses/InternalServerError"
/boroughs/{boroughId}/community-districts/{communityDistrictId}/capital-projects:
get:
summary: 🚧 Find paginated capital projects within a specified community district
summary: Find paginated capital projects within a specified community district
operationId: findCapitalProjectsByBoroughIdCommunityDistrictId
tags:
- Capital Projects
Expand Down Expand Up @@ -786,9 +786,9 @@ components:
type: string
nullable: true
enum:
- "Fixed Asset"
- "Lump Sum"
- "ITT, Vehicles and Equipment"
- Fixed Asset
- Lump Sum
- ITT, Vehicles and Equipment
- null
description: The type of Capital Project.
CapitalProject:
Expand Down
25 changes: 25 additions & 0 deletions src/borough/borough.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import {
Get,
Injectable,
Param,
Query,
UseFilters,
UsePipes,
} from "@nestjs/common";
import { BoroughService } from "./borough.service";
import {
FindCapitalProjectsByBoroughIdCommunityDistrictIdPathParams,
FindCapitalProjectsByBoroughIdCommunityDistrictIdQueryParams,
FindCommunityDistrictsByBoroughIdPathParams,
findCapitalProjectsByBoroughIdCommunityDistrictIdPathParamsSchema,
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryParamsSchema,
findCommunityDistrictsByBoroughIdPathParamsSchema,
} from "src/gen";
import {
Expand Down Expand Up @@ -44,4 +49,24 @@ export class BoroughController {
params.boroughId,
);
}

@Get("/:boroughId/community-districts/:communityDistrictId/capital-projects")
async findCapitalProjectsByBoroughIdCommunityDistrictId(
@Param(
new ZodTransformPipe(
findCapitalProjectsByBoroughIdCommunityDistrictIdPathParamsSchema,
),
)
pathParams: FindCapitalProjectsByBoroughIdCommunityDistrictIdPathParams,
@Query(
new ZodTransformPipe(
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryParamsSchema,
),
)
queryParams: FindCapitalProjectsByBoroughIdCommunityDistrictIdQueryParams,
) {
return this.boroughService.findCapitalProjectsByBoroughIdCommunityDistrictId(
{ ...pathParams, ...queryParams },
);
}
}
3 changes: 2 additions & 1 deletion src/borough/borough.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Module } from "@nestjs/common";
import { BoroughService } from "./borough.service";
import { BoroughController } from "./borough.controller";
import { BoroughRepository } from "./borough.repository";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";

@Module({
exports: [BoroughService],
providers: [BoroughService, BoroughRepository],
providers: [BoroughService, BoroughRepository, CommunityDistrictRepository],
controllers: [BoroughController],
})
export class BoroughModule {}
8 changes: 8 additions & 0 deletions src/borough/borough.repository.schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { capitalProjectSchema } from "src/gen";
import { boroughEntitySchema, communityDistrictEntitySchema } from "src/schema";
import { z } from "zod";

Expand All @@ -18,3 +19,10 @@ export const findCommunityDistrictsByBoroughIdRepoSchema = z.array(
export type FindCommunityDistrictsByBoroughIdRepo = z.infer<
typeof findCommunityDistrictsByBoroughIdRepoSchema
>;

export const findCapitalProjectsByBoroughIdCommunityDistrictIdRepoSchema =
z.array(capitalProjectSchema);

export type FindCapitalProjectsByBoroughIdCommunityDistrictIdRepo = z.infer<
typeof findCapitalProjectsByBoroughIdCommunityDistrictIdRepoSchema
>;
48 changes: 46 additions & 2 deletions src/borough/borough.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
CheckByIdRepo,
FindManyRepo,
FindCommunityDistrictsByBoroughIdRepo,
FindCapitalProjectsByBoroughIdCommunityDistrictIdRepo,
} from "./borough.repository.schema";
import { communityDistrict } from "src/schema";
import { eq } from "drizzle-orm";
import { capitalProject, communityDistrict } from "src/schema";
import { eq, sql, and } from "drizzle-orm";

export class BoroughRepository {
constructor(
Expand Down Expand Up @@ -57,4 +58,47 @@ export class BoroughRepository {
throw new DataRetrievalException();
}
}

async findCapitalProjectsByBoroughIdCommunityDistrictId({
boroughId,
communityDistrictId,
limit,
offset,
}: {
boroughId: string;
communityDistrictId: string;
limit: number;
offset: number;
}): Promise<FindCapitalProjectsByBoroughIdCommunityDistrictIdRepo> {
try {
return await this.db
.select({
id: capitalProject.id,
description: capitalProject.description,
managingCode: capitalProject.managingCode,
managingAgency: capitalProject.managingAgency,
maxDate: capitalProject.maxDate,
minDate: capitalProject.minDate,
category: capitalProject.category,
})
.from(capitalProject)
.leftJoin(
communityDistrict,
sql`
ST_Intersects(${communityDistrict.liFt}, ${capitalProject.liFtMPoly})
OR ST_Intersects(${communityDistrict.liFt}, ${capitalProject.liFtMPnt})`,
)
.where(
and(
eq(communityDistrict.boroughId, boroughId),
eq(communityDistrict.id, communityDistrictId),
),
)
.limit(limit)
.offset(offset)
.orderBy(capitalProject.managingCode, capitalProject.id);
} catch {
throw new DataRetrievalException();
}
}
}
70 changes: 68 additions & 2 deletions src/borough/borough.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { BoroughRepository } from "src/borough/borough.repository";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";
import { BoroughService } from "./borough.service";
import { BoroughRepositoryMock } from "../../test/borough/borough.repository.mock";
import { Test } from "@nestjs/testing";
import {
findBoroughsQueryResponseSchema,
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema,
findCommunityDistrictsByBoroughIdQueryResponseSchema,
} from "src/gen";
import { ResourceNotFoundException } from "src/exception";
import { CommunityDistrictRepositoryMock } from "test/community-district/community-district.repository.mock";

describe("Borough service unit", () => {
let boroughService: BoroughService;

const boroughRepositoryMock = new BoroughRepositoryMock();
const communityDistrictRepositoryMock = new CommunityDistrictRepositoryMock();
const boroughRepositoryMock = new BoroughRepositoryMock(
communityDistrictRepositoryMock,
);

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [BoroughService, BoroughRepository],
providers: [
BoroughService,
BoroughRepository,
CommunityDistrictRepository,
],
})
.overrideProvider(BoroughRepository)
.useValue(boroughRepositoryMock)
.overrideProvider(CommunityDistrictRepository)
.useValue(communityDistrictRepositoryMock)
.compile();

boroughService = moduleRef.get<BoroughService>(BoroughService);
Expand Down Expand Up @@ -53,4 +65,58 @@ describe("Borough service unit", () => {
expect(zoningDistrict).rejects.toThrow(ResourceNotFoundException);
});
});

describe("findCapitalProjectsByBoroughIdCommunityDistrictId", () => {
const boroughId = boroughRepositoryMock.checkBoroughByIdMocks[0].id;
const communityDistrictId =
boroughRepositoryMock.communityDistrictRepoMock
.checkCommunityDistrictByIdMocks[0].id;
it("service should return a capital projects compliant object using default query params", async () => {
const capitalProjects =
await boroughService.findCapitalProjectsByBoroughIdCommunityDistrictId({
boroughId,
communityDistrictId,
});

expect(() =>
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema.parse(
capitalProjects,
),
).not.toThrow();

const parsedBody =
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema.parse(
capitalProjects,
);
expect(parsedBody.limit).toBe(20);
expect(parsedBody.offset).toBe(0);
expect(parsedBody.total).toBe(parsedBody.capitalProjects.length);
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});

it("service should return a list of capital projects by community district id, using the user specified limit and offset", async () => {
const capitalProjects =
await boroughService.findCapitalProjectsByBoroughIdCommunityDistrictId({
boroughId,
communityDistrictId,
limit: 10,
offset: 3,
});

expect(() =>
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema.parse(
capitalProjects,
),
).not.toThrow();

const parsedBody =
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema.parse(
capitalProjects,
);
expect(parsedBody.limit).toBe(10);
expect(parsedBody.offset).toBe(3);
expect(parsedBody.total).toBe(parsedBody.capitalProjects.length);
expect(parsedBody.order).toBe("managingCode, capitalProjectId");
});
});
});
44 changes: 44 additions & 0 deletions src/borough/borough.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Inject, Injectable } from "@nestjs/common";
import { BoroughRepository } from "./borough.repository";
import { CommunityDistrictRepository } from "src/community-district/community-district.repository";
import { ResourceNotFoundException } from "src/exception";
import {
FindCapitalProjectsByBoroughIdCommunityDistrictIdPathParams,
FindCapitalProjectsByBoroughIdCommunityDistrictIdQueryParams,
} from "src/gen";

@Injectable()
export class BoroughService {
constructor(
@Inject(BoroughRepository)
private readonly boroughRepository: BoroughRepository,
@Inject(CommunityDistrictRepository)
private readonly communityDistrictRepository: CommunityDistrictRepository,
) {}

async findMany() {
Expand All @@ -27,4 +34,41 @@ export class BoroughService {
communityDistricts,
};
}

async findCapitalProjectsByBoroughIdCommunityDistrictId({
boroughId,
communityDistrictId,
limit = 20,
offset = 0,
}: FindCapitalProjectsByBoroughIdCommunityDistrictIdPathParams &
FindCapitalProjectsByBoroughIdCommunityDistrictIdQueryParams) {
const boroughCheck =
await this.boroughRepository.checkBoroughById(boroughId);
if (boroughCheck === undefined) throw new ResourceNotFoundException();

const communityDistrictCheck =
await this.communityDistrictRepository.checkCommunityDistrictById(
communityDistrictId,
);
if (communityDistrictCheck === undefined)
throw new ResourceNotFoundException();

const capitalProjects =
await this.boroughRepository.findCapitalProjectsByBoroughIdCommunityDistrictId(
{
boroughId,
communityDistrictId,
limit,
offset,
},
);

return {
limit,
offset,
total: capitalProjects.length,
order: "managingCode, capitalProjectId",
capitalProjects,
};
}
}
10 changes: 10 additions & 0 deletions src/community-district/community-district.repository.schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { communityDistrictEntitySchema } from "src/schema";
import { mvtEntitySchema } from "src/schema/mvt";
import { z } from "zod";

export const findTilesRepoSchema = mvtEntitySchema;

export type FindTilesRepo = z.infer<typeof findTilesRepoSchema>;

export const checkByCommunityDistrictIdRepoSchema =
communityDistrictEntitySchema.pick({
id: true,
});

export type CheckByCommunityDistrictIdRepo = z.infer<
typeof checkByCommunityDistrictIdRepoSchema
>;
27 changes: 26 additions & 1 deletion src/community-district/community-district.repository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Inject } from "@nestjs/common";
import { DB, DbType } from "src/global/providers/db.provider";
import { FindCommunityDistrictTilesPathParams } from "src/gen";
import { FindTilesRepo } from "./community-district.repository.schema";
import {
FindTilesRepo,
CheckByCommunityDistrictIdRepo,
} from "./community-district.repository.schema";
import { borough, communityDistrict } from "src/schema";
import { sql, isNotNull, eq } from "drizzle-orm";
import { DataRetrievalException } from "src/exception";
Expand All @@ -12,6 +15,28 @@ export class CommunityDistrictRepository {
private readonly db: DbType,
) {}

#checkCommunityDistrictById = this.db.query.communityDistrict
.findFirst({
columns: {
id: true,
},
where: (communityDistrict, { eq, sql }) =>
eq(communityDistrict.id, sql.placeholder("id")),
})
.prepare("checkCommunityDistrictId");

async checkCommunityDistrictById(
id: string,
): Promise<CheckByCommunityDistrictIdRepo | undefined> {
try {
return await this.#checkCommunityDistrictById.execute({
id,
});
} catch {
throw new DataRetrievalException();
}
}

async findTiles(
params: FindCommunityDistrictTilesPathParams,
): Promise<FindTilesRepo> {
Expand Down
1 change: 1 addition & 0 deletions src/schema/community-district.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const communityDistrict = pgTable(
(table) => {
return {
pk: primaryKey({ columns: [table.boroughId, table.id] }),
liFtGix: index().using("GIST", table.liFt),
mercatorFillGix: index().using("GIST", table.mercatorFill),
mercatorLabelGix: index().using("GIST", table.mercatorLabel),
};
Expand Down
Loading

0 comments on commit a796442

Please sign in to comment.