From e038daf68bf5eed59977af7192308fd15446fe45 Mon Sep 17 00:00:00 2001 From: David Hochbaum Date: Thu, 30 Jan 2025 15:52:22 -0500 Subject: [PATCH] Added /capital-projects/ endpoint --- openapi/openapi.yaml | 2 + openapi/paths/capital-projects.yaml | 23 +++++++++ .../capital-project.controller.ts | 12 +++++ .../capital-project.repository.schema.ts | 8 +++ .../capital-project.repository.ts | 28 +++++++++++ .../capital-project.service.spec.ts | 11 +++++ .../capital-project.service.ts | 11 +++++ src/gen/types/FindCapitalProjects.ts | 43 ++++++++++++++++ src/gen/types/index.ts | 1 + src/gen/zod/findCapitalProjectsSchema.ts | 49 +++++++++++++++++++ src/gen/zod/index.ts | 1 + src/gen/zod/operations.ts | 30 ++++++++++++ .../capital-project.repository.mock.ts | 22 +++++++++ 13 files changed, 241 insertions(+) create mode 100644 openapi/paths/capital-projects.yaml create mode 100644 src/gen/types/FindCapitalProjects.ts create mode 100644 src/gen/zod/findCapitalProjectsSchema.ts diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index dc4184d..000a84a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -57,6 +57,8 @@ paths: paths/boroughs_{boroughId}_community-districts_{communityDistrictId}_capital-projects_{z}_{x}_{y}.pbf.yaml /capital-commitment-types: $ref: paths/capital-commitment-types.yaml + /capital-projects: + $ref: paths/capital-projects.yaml /capital-projects/{managingCode}/{capitalProjectId}/capital-commitments: $ref: >- paths/capital-projects_{managingCode}_{capitalProjectId}_capital-commitments.yaml diff --git a/openapi/paths/capital-projects.yaml b/openapi/paths/capital-projects.yaml new file mode 100644 index 0000000..49bd98c --- /dev/null +++ b/openapi/paths/capital-projects.yaml @@ -0,0 +1,23 @@ +get: + summary: Find paginated capital projects. + operationId: findCapitalProjects + tags: + - Capital Projects + parameters: + - $ref: ../components/parameters/limitParam.yaml + - $ref: ../components/parameters/offsetParam.yaml + responses: + '200': + description: >- + An object containing pagination metadata and an array of capital + projects + content: + application/json: + schema: + $ref: ../components/schemas/CapitalProjectPage.yaml + '400': + $ref: ../components/responses/BadRequest.yaml + '404': + $ref: ../components/responses/NotFound.yaml + '500': + $ref: ../components/responses/InternalServerError.yaml diff --git a/src/capital-project/capital-project.controller.ts b/src/capital-project/capital-project.controller.ts index 91cc9db..33d3d4d 100644 --- a/src/capital-project/capital-project.controller.ts +++ b/src/capital-project/capital-project.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, + Query, Res, UseFilters, UsePipes, @@ -12,6 +13,8 @@ import { FindCapitalProjectByManagingCodeCapitalProjectIdPathParams, FindCapitalProjectGeoJsonByManagingCodeCapitalProjectIdPathParams, FindCapitalProjectTilesPathParams, + FindCapitalProjectsQueryParams, + findCapitalProjectsQueryParamsSchema, findCapitalCommitmentsByManagingCodeCapitalProjectIdPathParamsSchema, findCapitalProjectByManagingCodeCapitalProjectIdPathParamsSchema, findCapitalProjectGeoJsonByManagingCodeCapitalProjectIdPathParamsSchema, @@ -33,6 +36,15 @@ import { ZodTransformPipe } from "src/pipes/zod-transform-pipe"; export class CapitalProjectController { constructor(private readonly capitalProjectService: CapitalProjectService) {} + @UsePipes(new ZodTransformPipe(findCapitalProjectsQueryParamsSchema)) + @Get("/") + async findCapitalProjects( + @Query(new ZodTransformPipe(findCapitalProjectsQueryParamsSchema)) + queryParams: FindCapitalProjectsQueryParams, + ) { + return await this.capitalProjectService.findCapitalProjects(queryParams); + } + @UsePipes( new ZodTransformPipe( findCapitalProjectByManagingCodeCapitalProjectIdPathParamsSchema, diff --git a/src/capital-project/capital-project.repository.schema.ts b/src/capital-project/capital-project.repository.schema.ts index 8835438..85bf39c 100644 --- a/src/capital-project/capital-project.repository.schema.ts +++ b/src/capital-project/capital-project.repository.schema.ts @@ -10,6 +10,14 @@ import { import { mvtEntitySchema } from "src/schema/mvt"; import { z } from "zod"; +export const findCapitalProjectsRepoSchema = z.array( + capitalProjectEntitySchema, +); + +export type FindCapitalProjectsRepo = z.infer< + typeof findCapitalProjectsRepoSchema +>; + export const checkByManagingCodeCapitalProjectIdRepoSchema = capitalProjectEntitySchema.pick({ id: true, diff --git a/src/capital-project/capital-project.repository.ts b/src/capital-project/capital-project.repository.ts index c08ae6c..62cadd6 100644 --- a/src/capital-project/capital-project.repository.ts +++ b/src/capital-project/capital-project.repository.ts @@ -21,6 +21,7 @@ import { FindByManagingCodeCapitalProjectIdRepo, FindCapitalCommitmentsByManagingCodeCapitalProjectIdRepo, FindGeoJsonByManagingCodeCapitalProjectIdRepo, + FindCapitalProjectsRepo, FindTilesRepo, } from "./capital-project.repository.schema"; @@ -30,6 +31,33 @@ export class CapitalProjectRepository { private readonly db: DbType, ) {} + async findCapitalProjects({ + limit, + offset, + }: { + limit: number; + offset: number; + }): Promise { + 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: sql`${capitalProject.category}`, + }) + .from(capitalProject) + .limit(limit) + .offset(offset) + .orderBy(capitalProject.managingCode, capitalProject.id); + } catch { + throw new DataRetrievalException(); + } + } + #checkByManagingCodeCapitalProjectId = this.db.query.capitalProject .findFirst({ columns: { diff --git a/src/capital-project/capital-project.service.spec.ts b/src/capital-project/capital-project.service.spec.ts index 3fa63b1..067b7aa 100644 --- a/src/capital-project/capital-project.service.spec.ts +++ b/src/capital-project/capital-project.service.spec.ts @@ -7,6 +7,7 @@ import { findCapitalProjectByManagingCodeCapitalProjectIdQueryResponseSchema, findCapitalProjectGeoJsonByManagingCodeCapitalProjectIdQueryResponseSchema, findCapitalProjectTilesQueryResponseSchema, + findCapitalProjectsQueryResponseSchema, } from "src/gen"; import { ResourceNotFoundException } from "src/exception"; @@ -28,6 +29,16 @@ describe("CapitalProjectService", () => { ); }); + describe("findCapitalProjects", () => { + it("should return a list of capital projects", async () => { + const capitalProjectsResponse = + await capitalProjectService.findCapitalProjects({}); + expect(() => + findCapitalProjectsQueryResponseSchema.parse(capitalProjectsResponse), + ).not.toThrow(); + }); + }); + describe("findByManagingCodeCapitalProjectId", () => { it("should return a capital project with budget details", async () => { const capitalProjectMock = diff --git a/src/capital-project/capital-project.service.ts b/src/capital-project/capital-project.service.ts index e568418..52d64bf 100644 --- a/src/capital-project/capital-project.service.ts +++ b/src/capital-project/capital-project.service.ts @@ -4,6 +4,7 @@ import { FindCapitalProjectByManagingCodeCapitalProjectIdPathParams, FindCapitalProjectGeoJsonByManagingCodeCapitalProjectIdPathParams, FindCapitalProjectTilesPathParams, + FindCapitalProjectsQueryParams, } from "src/gen"; import { CapitalProjectRepository } from "./capital-project.repository"; import { Inject } from "@nestjs/common"; @@ -19,6 +20,16 @@ export class CapitalProjectService { private readonly capitalProjectRepository: CapitalProjectRepository, ) {} + async findCapitalProjects({ + limit = 20, + offset = 0, + }: FindCapitalProjectsQueryParams) { + return await this.capitalProjectRepository.findCapitalProjects({ + limit, + offset, + }); + } + async findByManagingCodeCapitalProjectId( params: FindCapitalProjectByManagingCodeCapitalProjectIdPathParams, ) { diff --git a/src/gen/types/FindCapitalProjects.ts b/src/gen/types/FindCapitalProjects.ts new file mode 100644 index 0000000..2c6fc1f --- /dev/null +++ b/src/gen/types/FindCapitalProjects.ts @@ -0,0 +1,43 @@ +import type { CapitalProjectPage } from "./CapitalProjectPage"; +import type { Error } from "./Error"; + +export type FindCapitalProjectsQueryParams = { + /** + * @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 + */ + limit?: number; + /** + * @description The position in the full list to begin returning results. Default offset is 0. If the offset is beyond the end of the list, no results will be returned. + * @type integer | undefined + */ + offset?: number; +}; +/** + * @description An object containing pagination metadata and an array of capital projects + */ +export type FindCapitalProjects200 = CapitalProjectPage; +/** + * @description Invalid client request + */ +export type FindCapitalProjects400 = Error; +/** + * @description Requested resource does not exist or is not available + */ +export type FindCapitalProjects404 = Error; +/** + * @description Server side error + */ +export type FindCapitalProjects500 = Error; +/** + * @description An object containing pagination metadata and an array of capital projects + */ +export type FindCapitalProjectsQueryResponse = CapitalProjectPage; +export type FindCapitalProjectsQuery = { + Response: FindCapitalProjectsQueryResponse; + QueryParams: FindCapitalProjectsQueryParams; + Errors: + | FindCapitalProjects400 + | FindCapitalProjects404 + | FindCapitalProjects500; +}; diff --git a/src/gen/types/index.ts b/src/gen/types/index.ts index 0399b56..c483a7e 100644 --- a/src/gen/types/index.ts +++ b/src/gen/types/index.ts @@ -24,6 +24,7 @@ export * from "./FindCapitalProjectGeoJsonByManagingCodeCapitalProjectId"; export * from "./FindCapitalProjectTiles"; export * from "./FindCapitalProjectTilesByBoroughIdCommunityDistrictId"; export * from "./FindCapitalProjectTilesByCityCouncilDistrictId"; +export * from "./FindCapitalProjects"; export * from "./FindCapitalProjectsByBoroughIdCommunityDistrictId"; export * from "./FindCapitalProjectsByCityCouncilId"; export * from "./FindCityCouncilDistrictGeoJsonByCityCouncilDistrictId"; diff --git a/src/gen/zod/findCapitalProjectsSchema.ts b/src/gen/zod/findCapitalProjectsSchema.ts new file mode 100644 index 0000000..0935be3 --- /dev/null +++ b/src/gen/zod/findCapitalProjectsSchema.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { capitalProjectPageSchema } from "./capitalProjectPageSchema"; +import { errorSchema } from "./errorSchema"; + +export const findCapitalProjectsQueryParamsSchema = z + .object({ + limit: z.coerce + .number() + .int() + .min(1) + .max(100) + .describe( + "The maximum number of results to be returned in each response. The default value is 20. It must be between 1 and 100, inclusive.", + ) + .optional(), + offset: z.coerce + .number() + .int() + .min(0) + .describe( + "The position in the full list to begin returning results. Default offset is 0. If the offset is beyond the end of the list, no results will be returned.", + ) + .optional(), + }) + .optional(); +/** + * @description An object containing pagination metadata and an array of capital projects + */ +export const findCapitalProjects200Schema = z.lazy( + () => capitalProjectPageSchema, +); +/** + * @description Invalid client request + */ +export const findCapitalProjects400Schema = z.lazy(() => errorSchema); +/** + * @description Requested resource does not exist or is not available + */ +export const findCapitalProjects404Schema = z.lazy(() => errorSchema); +/** + * @description Server side error + */ +export const findCapitalProjects500Schema = z.lazy(() => errorSchema); +/** + * @description An object containing pagination metadata and an array of capital projects + */ +export const findCapitalProjectsQueryResponseSchema = z.lazy( + () => capitalProjectPageSchema, +); diff --git a/src/gen/zod/index.ts b/src/gen/zod/index.ts index d2b036c..9ff77b5 100644 --- a/src/gen/zod/index.ts +++ b/src/gen/zod/index.ts @@ -26,6 +26,7 @@ export * from "./findCapitalProjectTilesByCityCouncilDistrictIdSchema"; export * from "./findCapitalProjectTilesSchema"; export * from "./findCapitalProjectsByBoroughIdCommunityDistrictIdSchema"; export * from "./findCapitalProjectsByCityCouncilIdSchema"; +export * from "./findCapitalProjectsSchema"; export * from "./findCityCouncilDistrictGeoJsonByCityCouncilDistrictIdSchema"; export * from "./findCityCouncilDistrictTilesSchema"; export * from "./findCityCouncilDistrictsSchema"; diff --git a/src/gen/zod/operations.ts b/src/gen/zod/operations.ts index 4cd0e67..15434f8 100644 --- a/src/gen/zod/operations.ts +++ b/src/gen/zod/operations.ts @@ -46,6 +46,13 @@ import { findCapitalCommitmentTypes400Schema, findCapitalCommitmentTypes500Schema, } from "./findCapitalCommitmentTypesSchema"; +import { + findCapitalProjectsQueryResponseSchema, + findCapitalProjects400Schema, + findCapitalProjects404Schema, + findCapitalProjects500Schema, + findCapitalProjectsQueryParamsSchema, +} from "./findCapitalProjectsSchema"; import { findCapitalCommitmentsByManagingCodeCapitalProjectIdQueryResponseSchema, findCapitalCommitmentsByManagingCodeCapitalProjectId400Schema, @@ -336,6 +343,26 @@ export const operations = { 500: findCapitalCommitmentTypes500Schema, }, }, + findCapitalProjects: { + request: undefined, + parameters: { + path: undefined, + query: findCapitalProjectsQueryParamsSchema, + header: undefined, + }, + responses: { + 200: findCapitalProjectsQueryResponseSchema, + 400: findCapitalProjects400Schema, + 404: findCapitalProjects404Schema, + 500: findCapitalProjects500Schema, + default: findCapitalProjectsQueryResponseSchema, + }, + errors: { + 400: findCapitalProjects400Schema, + 404: findCapitalProjects404Schema, + 500: findCapitalProjects500Schema, + }, + }, findCapitalCommitmentsByManagingCodeCapitalProjectId: { request: undefined, parameters: { @@ -774,6 +801,9 @@ export const paths = { "/capital-commitment-types": { get: operations["findCapitalCommitmentTypes"], }, + "/capital-projects": { + get: operations["findCapitalProjects"], + }, "/capital-projects/{managingCode}/{capitalProjectId}/capital-commitments": { get: operations["findCapitalCommitmentsByManagingCodeCapitalProjectId"], }, diff --git a/test/capital-project/capital-project.repository.mock.ts b/test/capital-project/capital-project.repository.mock.ts index f64f2f8..a0b2420 100644 --- a/test/capital-project/capital-project.repository.mock.ts +++ b/test/capital-project/capital-project.repository.mock.ts @@ -8,6 +8,7 @@ import { FindGeoJsonByManagingCodeCapitalProjectIdRepo, findGeoJsonByManagingCodeCapitalProjectIdRepoSchema, findTilesRepoSchema, + findCapitalProjectsRepoSchema, } from "src/capital-project/capital-project.repository.schema"; import { FindCapitalCommitmentsByManagingCodeCapitalProjectIdPathParams, @@ -16,6 +17,27 @@ import { } from "src/gen"; export class CapitalProjectRepositoryMock { + findCapitalProjectsMocks = Array.from( + Array(4), + (_) => + generateMock(findCapitalProjectsRepoSchema, { + stringMap: { + minDate: () => "2018-01-01", + maxDate: () => "2045-12-31", + }, + })[0], + ); + + async findCapitalProjects() { + return { + limit: 20, + offset: 0, + total: 4, + order: "managingCode, capitalProjectId", + capitalProjects: this.findCapitalProjectsMocks, + }; + } + checkByManagingCodeCapitalProjectIdMocks = Array.from(Array(5), (_, seed) => generateMock(checkByManagingCodeCapitalProjectIdRepoSchema, { seed: seed + 1,