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
  • Loading branch information
pratishta committed Jun 26, 2024
1 parent 3860153 commit a2a9cd8
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 6 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 },
);
}
}
17 changes: 17 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 @@ -11,10 +12,26 @@ export const checkByIdRepoSchema = boroughEntitySchema.pick({

export type CheckByIdRepo = z.infer<typeof checkByIdRepoSchema>;

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

export type CheckByCommunityDistrictIdRepo = z.infer<
typeof checkByCommunityDistrictIdRepoSchema
>;

export const findCommunityDistrictsByBoroughIdRepoSchema = z.array(
communityDistrictEntitySchema,
);

export type FindCommunityDistrictsByBoroughIdRepo = z.infer<
typeof findCommunityDistrictsByBoroughIdRepoSchema
>;

export const findCapitalProjectsByBoroughIdCommunityDistrictIdRepoSchema =
z.array(capitalProjectSchema);

export type FindCapitalProjectsByBoroughIdCommunityDistrictIdRepo = z.infer<
typeof findCapitalProjectsByBoroughIdCommunityDistrictIdRepoSchema
>;
70 changes: 68 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 All @@ -34,6 +35,28 @@ export class BoroughRepository {
}
}

#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<CheckByIdRepo | undefined> {
try {
return await this.#checkCommunityDistrictById.execute({
id,
});
} catch {
throw new DataRetrievalException();
}
}

async findMany(): Promise<FindManyRepo> {
try {
return await this.db.query.borough.findMany();
Expand All @@ -57,4 +80,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();
}
}
}
21 changes: 21 additions & 0 deletions src/borough/borough.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BoroughRepositoryMock } from "../../test/borough/borough.repository.moc
import { Test } from "@nestjs/testing";
import {
findBoroughsQueryResponseSchema,
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema,
findCommunityDistrictsByBoroughIdQueryResponseSchema,
} from "src/gen";
import { ResourceNotFoundException } from "src/exception";
Expand Down Expand Up @@ -53,4 +54,24 @@ describe("Borough service unit", () => {
expect(zoningDistrict).rejects.toThrow(ResourceNotFoundException);
});
});

describe("findCapitalProjectsByBoroughIdCommunityDistrictId", () => {
it("service should return a capital projects compliant object", async () => {
const boroughId = boroughRepositoryMock.checkBoroughByIdMocks[0].id;
const communityDistrictId =
boroughRepositoryMock.checkCommunityDistrictByIdMocks[0].id;

const capitalProjects =
await boroughService.findCapitalProjectsByBoroughIdCommunityDistrictId({
boroughId,
communityDistrictId,
});

expect(() =>
findCapitalProjectsByBoroughIdCommunityDistrictIdQueryResponseSchema.parse(
capitalProjects,
),
).not.toThrow();
});
});
});
41 changes: 41 additions & 0 deletions src/borough/borough.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Inject, Injectable } from "@nestjs/common";
import { BoroughRepository } from "./borough.repository";
import { ResourceNotFoundException } from "src/exception";
import {
FindCapitalProjectsByBoroughIdCommunityDistrictIdPathParams,
FindCapitalProjectsByBoroughIdCommunityDistrictIdQueryParams,
} from "src/gen";

@Injectable()
export class BoroughService {
Expand All @@ -27,4 +31,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.boroughRepository.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,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
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 checkByIdRepoSchema = communityDistrictEntitySchema.pick({
id: true,
});

export type CheckByIdRepo = z.infer<typeof checkByIdRepoSchema>;
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 a2a9cd8

Please sign in to comment.