diff --git a/README.md b/README.md index e2842140..0a8bbd32 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ Finally, to run the api locally: npm run dev ``` +- View the documentation locally via `localhost:3000` +- To use the API locally, append `/api/{endpoint_route}` to the localhost port (ex: `localhost:3000/api/boroughs`) + (This command will also create a static site of the OpenAPI documentation at the root of the API. This site reflects the documentation at the point where the command was written. Viewing changes to the OpenAPI documentation requires restarting the development server). diff --git a/compose.yaml b/compose.yaml index f89472ce..6977be35 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,5 +10,9 @@ services: - POSTGRES_DB=${DATABASE_NAME} ports: - "8010:5432" + networks: + - data volumes: - ./db-volume:/var/lib/postgresql/data +networks: + data: diff --git a/src/app.module.ts b/src/app.module.ts index 7dd334e8..b14296d7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { DbConfig, FeatureFlagConfig, StorageConfig } from "./config"; import { GlobalModule } from "./global/global.module"; import { ZoningDistrictClassModule } from "./zoning-district-class/zoning-district-class.module"; import { AgencyModule } from "./agency/agency.module"; +import { CityCouncilDistrictModule } from "./city-council-district/city-council-district.module"; @Module({ imports: [ @@ -39,6 +40,7 @@ import { AgencyModule } from "./agency/agency.module"; GlobalModule, AgencyModule, BoroughModule, + CityCouncilDistrictModule, LandUseModule, TaxLotModule, ZoningDistrictModule, diff --git a/src/city-council-district/city-council-district.controller.ts b/src/city-council-district/city-council-district.controller.ts new file mode 100644 index 00000000..49f38b89 --- /dev/null +++ b/src/city-council-district/city-council-district.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get, UseFilters } from "@nestjs/common"; +import { CityCouncilDistrictService } from "./city-council-district.service"; +import { InternalServerErrorExceptionFilter } from "src/filter"; + +@UseFilters(InternalServerErrorExceptionFilter) +@Controller("city-council-districts") +export class CityCouncilDistrictController { + constructor( + private readonly cityCouncilDistrictService: CityCouncilDistrictService, + ) {} + + @Get() + async findMany() { + return this.cityCouncilDistrictService.findMany(); + } +} diff --git a/src/city-council-district/city-council-district.module.ts b/src/city-council-district/city-council-district.module.ts new file mode 100644 index 00000000..f00df848 --- /dev/null +++ b/src/city-council-district/city-council-district.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { CityCouncilDistrictService } from "./city-council-district.service"; +import { CityCouncilDistrictController } from "./city-council-district.controller"; +import { CityCouncilDistrictRepository } from "./city-council-district.repository"; + +@Module({ + exports: [CityCouncilDistrictService], + providers: [CityCouncilDistrictService, CityCouncilDistrictRepository], + controllers: [CityCouncilDistrictController], +}) +export class CityCouncilDistrictModule {} diff --git a/src/city-council-district/city-council-district.repository.schema.ts b/src/city-council-district/city-council-district.repository.schema.ts new file mode 100644 index 00000000..3a44b62c --- /dev/null +++ b/src/city-council-district/city-council-district.repository.schema.ts @@ -0,0 +1,6 @@ +import { cityCouncilDistrictEntitySchema } from "src/schema"; +import { z } from "zod"; + +export const findManyRepoSchema = z.array(cityCouncilDistrictEntitySchema); + +export type FindManyRepo = z.infer; diff --git a/src/city-council-district/city-council-district.repository.ts b/src/city-council-district/city-council-district.repository.ts new file mode 100644 index 00000000..4c7cf474 --- /dev/null +++ b/src/city-council-district/city-council-district.repository.ts @@ -0,0 +1,23 @@ +import { Inject } from "@nestjs/common"; +import { DB, DbType } from "src/global/providers/db.provider"; +import { DataRetrievalException } from "src/exception"; +import { FindManyRepo } from "./city-council-district.repository.schema"; + +export class CityCouncilDistrictRepository { + constructor( + @Inject(DB) + private readonly db: DbType, + ) {} + + async findMany(): Promise { + try { + return await this.db.query.cityCouncilDistrict.findMany({ + columns: { + id: true, + }, + }); + } catch { + throw new DataRetrievalException(); + } + } +} diff --git a/src/city-council-district/city-council-district.service.spec.ts b/src/city-council-district/city-council-district.service.spec.ts new file mode 100644 index 00000000..c3067415 --- /dev/null +++ b/src/city-council-district/city-council-district.service.spec.ts @@ -0,0 +1,32 @@ +import { CityCouncilDistrictRepositoryMock } from "test/city-council-district/city-council-district.repository.mock"; +import { Test } from "@nestjs/testing"; +import { CityCouncilDistrictRepository } from "./city-council-district.repository"; +import { findCityCouncilDistrictsQueryResponseSchema } from "src/gen"; +import { CityCouncilDistrictService } from "./city-council-district.service"; + +describe("City Council District service unit", () => { + let cityCouncilDistrictService: CityCouncilDistrictService; + + beforeEach(async () => { + const cityCouncilDistrictRepositoryMock = + new CityCouncilDistrictRepositoryMock(); + + const moduleRef = await Test.createTestingModule({ + providers: [CityCouncilDistrictService, CityCouncilDistrictRepository], + }) + .overrideProvider(CityCouncilDistrictRepository) + .useValue(cityCouncilDistrictRepositoryMock) + .compile(); + + cityCouncilDistrictService = moduleRef.get( + CityCouncilDistrictService, + ); + }); + + it("service should return a findCityCouncilDistrictsQueryResponseSchema compliant object", async () => { + const cityCouncilDistricts = await cityCouncilDistrictService.findMany(); + expect(() => + findCityCouncilDistrictsQueryResponseSchema.parse(cityCouncilDistricts), + ).not.toThrow(); + }); +}); diff --git a/src/city-council-district/city-council-district.service.ts b/src/city-council-district/city-council-district.service.ts new file mode 100644 index 00000000..d7bb6d17 --- /dev/null +++ b/src/city-council-district/city-council-district.service.ts @@ -0,0 +1,19 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { CityCouncilDistrictRepository } from "./city-council-district.repository"; + +@Injectable() +export class CityCouncilDistrictService { + constructor( + @Inject(CityCouncilDistrictRepository) + private readonly cityCouncilDistrictRepository: CityCouncilDistrictRepository, + ) {} + + async findMany() { + const cityCouncilDistricts = + await this.cityCouncilDistrictRepository.findMany(); + + return { + cityCouncilDistricts, + }; + } +} diff --git a/src/gen/types/CapitalCommitment.ts b/src/gen/types/CapitalCommitment.ts index 1e1058d5..b599d2b1 100644 --- a/src/gen/types/CapitalCommitment.ts +++ b/src/gen/types/CapitalCommitment.ts @@ -1,50 +1,49 @@ export type CapitalCommitment = { /** * @description A uuid used to refer to the capital commitment. - * @type string | undefined uuid + * @type string uuid */ - id?: string; + id: string; /** * @description A four character string used to refer to the commitment type. - * @type string | undefined + * @type string * @example DSGN */ - type?: string; + type: string; /** * @description A string used to refer to the date when the commitment is projected to be committed. - * @type string | undefined date + * @type string date * @example 2012-04-23 */ - plannedDate?: string; + plannedDate: string; /** * @description A string used to refer to the budget line. - * @type string | undefined + * @type string * @example HW */ - budgetLineCode?: string; + budgetLineCode: string; /** * @description A string used to refer to the budget line. - * @type string | undefined + * @type string * @example 0002Q */ - budgetLineId?: string; + budgetLineId: string; /** * @description A string of variable length containing the initials of the sponsoring agency. - * @type string | undefined + * @type string * @example DOT */ - sponsoringAgencyInitials?: string; + sponsoringAgencyInitials: string; /** * @description A string of variable length denoting the type of budget. - * @type string | undefined + * @type string * @example Highways */ - budgetType?: string; + budgetType: string; /** * @description A numeric string used to refer to the amount of total planned commitments. - * @type number | undefined + * @type number * @example 1600000 */ - totalValue?: number; - required?: any; + totalValue: number; }; diff --git a/src/gen/types/CapitalProject.ts b/src/gen/types/CapitalProject.ts index acb3eeb4..bbb1656d 100644 --- a/src/gen/types/CapitalProject.ts +++ b/src/gen/types/CapitalProject.ts @@ -27,15 +27,15 @@ export type CapitalProject = { managingAgencyInitials: string; /** * @description The starting date of the capital project - * @type date - * @example 2024-05-15T14:20:03.842Z + * @type string date + * @example 2024-05-15 */ - minDate: any; + minDate: string; /** * @description The ending date of the capital project - * @type date - * @example 2024-05-15T14:20:03.842Z + * @type string date + * @example 2024-05-15 */ - maxDate: any; + maxDate: string; category?: CapitalProjectCategory; }; diff --git a/src/gen/types/CapitalProjectPage.ts b/src/gen/types/CapitalProjectPage.ts new file mode 100644 index 00000000..57bb3cad --- /dev/null +++ b/src/gen/types/CapitalProjectPage.ts @@ -0,0 +1,9 @@ +import type { Page } from "./Page"; +import type { CapitalProject } from "./CapitalProject"; + +export type CapitalProjectPage = Page & { + /** + * @type array + */ + capitalProjects: CapitalProject[]; +}; diff --git a/src/gen/types/CommunityDistrict.ts b/src/gen/types/CommunityDistrict.ts index 21a222bc..130d490d 100644 --- a/src/gen/types/CommunityDistrict.ts +++ b/src/gen/types/CommunityDistrict.ts @@ -1,15 +1,14 @@ export type CommunityDistrict = { /** * @description The two character numeric string containing the number used to refer to the community district. - * @type string | undefined + * @type string * @example 1 */ - id?: string; + id: string; /** * @description A single character numeric string containing the common number used to refer to the borough. Possible values are 1-5. - * @type string | undefined + * @type string * @example 1 */ - boroughId?: string; - required?: any; + boroughId: string; }; diff --git a/src/gen/types/FindCityCouncilDistricts.ts b/src/gen/types/FindCityCouncilDistricts.ts new file mode 100644 index 00000000..395e6f4d --- /dev/null +++ b/src/gen/types/FindCityCouncilDistricts.ts @@ -0,0 +1,16 @@ +import type { Error } from "./Error"; +import type { CityCouncilDistrict } from "./CityCouncilDistrict"; + +export type FindCityCouncilDistricts400 = Error; + +export type FindCityCouncilDistricts500 = Error; + +/** + * @description an object of city council districts + */ +export type FindCityCouncilDistrictsQueryResponse = { + /** + * @type array + */ + cityCouncilDistricts: CityCouncilDistrict[]; +}; diff --git a/src/gen/types/index.ts b/src/gen/types/index.ts index 6e85f09b..8d7cf28b 100644 --- a/src/gen/types/index.ts +++ b/src/gen/types/index.ts @@ -5,11 +5,13 @@ export * from "./CapitalCommitment"; export * from "./CapitalProject"; export * from "./CapitalProjectBudgeted"; export * from "./CapitalProjectCategory"; +export * from "./CapitalProjectPage"; export * from "./CityCouncilDistrict"; export * from "./CommunityDistrict"; export * from "./Error"; export * from "./FindAgencies"; export * from "./FindBoroughs"; +export * from "./FindCityCouncilDistricts"; export * from "./FindCommunityDistrictsByBoroughId"; export * from "./FindLandUses"; export * from "./FindTaxLotByBbl"; diff --git a/src/gen/zod/capitalCommitmentSchema.ts b/src/gen/zod/capitalCommitmentSchema.ts index 735f42f7..cba675cb 100644 --- a/src/gen/zod/capitalCommitmentSchema.ts +++ b/src/gen/zod/capitalCommitmentSchema.ts @@ -4,42 +4,33 @@ export const capitalCommitmentSchema = z.object({ id: z .string() .describe(`A uuid used to refer to the capital commitment.`) - .uuid() - .optional(), + .uuid(), type: z .string() .describe(`A four character string used to refer to the commitment type.`) - .regex(new RegExp("^([A-z]{4})$")) - .optional(), + .regex(new RegExp("^([A-z]{4})$")), plannedDate: z .string() .describe( `A string used to refer to the date when the commitment is projected to be committed.`, - ) - .optional(), + ), budgetLineCode: z .string() - .describe(`A string used to refer to the budget line.`) - .optional(), + .describe(`A string used to refer to the budget line.`), budgetLineId: z .string() - .describe(`A string used to refer to the budget line.`) - .optional(), + .describe(`A string used to refer to the budget line.`), sponsoringAgencyInitials: z .string() .describe( `A string of variable length containing the initials of the sponsoring agency.`, - ) - .optional(), + ), budgetType: z .string() - .describe(`A string of variable length denoting the type of budget.`) - .optional(), + .describe(`A string of variable length denoting the type of budget.`), totalValue: z .number() .describe( `A numeric string used to refer to the amount of total planned commitments.`, - ) - .optional(), - required: z.any().optional(), + ), }); diff --git a/src/gen/zod/capitalProjectPageSchema.ts b/src/gen/zod/capitalProjectPageSchema.ts new file mode 100644 index 00000000..ea948730 --- /dev/null +++ b/src/gen/zod/capitalProjectPageSchema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +import { pageSchema } from "./pageSchema"; +import { capitalProjectSchema } from "./capitalProjectSchema"; + +export const capitalProjectPageSchema = z + .lazy(() => pageSchema) + .schema.and( + z.object({ + capitalProjects: z.array(z.lazy(() => capitalProjectSchema).schema), + }), + ); diff --git a/src/gen/zod/capitalProjectSchema.ts b/src/gen/zod/capitalProjectSchema.ts index d4eb07b1..aa46181a 100644 --- a/src/gen/zod/capitalProjectSchema.ts +++ b/src/gen/zod/capitalProjectSchema.ts @@ -16,7 +16,7 @@ export const capitalProjectSchema = z.object({ managingAgencyInitials: z .string() .describe(`The managing agency name abbreviation or acronym`), - minDate: z.any().describe(`The starting date of the capital project`), - maxDate: z.any().describe(`The ending date of the capital project`), + minDate: z.string().describe(`The starting date of the capital project`), + maxDate: z.string().describe(`The ending date of the capital project`), category: z.lazy(() => capitalProjectCategorySchema).schema.optional(), }); diff --git a/src/gen/zod/communityDistrictSchema.ts b/src/gen/zod/communityDistrictSchema.ts index 8011815e..058c3293 100644 --- a/src/gen/zod/communityDistrictSchema.ts +++ b/src/gen/zod/communityDistrictSchema.ts @@ -6,14 +6,11 @@ export const communityDistrictSchema = z.object({ .describe( `The two character numeric string containing the number used to refer to the community district.`, ) - .regex(new RegExp("^([0-9]{2})$")) - .optional(), + .regex(new RegExp("^([0-9]{2})$")), boroughId: z .string() .describe( `A single character numeric string containing the common number used to refer to the borough. Possible values are 1-5.`, ) - .regex(new RegExp("\\b[1-9]\\b")) - .optional(), - required: z.any().optional(), + .regex(new RegExp("\\b[1-9]\\b")), }); diff --git a/src/gen/zod/findCityCouncilDistrictsSchema.ts b/src/gen/zod/findCityCouncilDistrictsSchema.ts new file mode 100644 index 00000000..a0431322 --- /dev/null +++ b/src/gen/zod/findCityCouncilDistrictsSchema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { errorSchema } from "./errorSchema"; +import { cityCouncilDistrictSchema } from "./cityCouncilDistrictSchema"; + +export const findCityCouncilDistricts400Schema = z.lazy( + () => errorSchema, +).schema; +export const findCityCouncilDistricts500Schema = z.lazy( + () => errorSchema, +).schema; + +/** + * @description an object of city council districts + */ +export const findCityCouncilDistrictsQueryResponseSchema = z.object({ + cityCouncilDistricts: z.array(z.lazy(() => cityCouncilDistrictSchema).schema), +}); diff --git a/src/gen/zod/index.ts b/src/gen/zod/index.ts index ad6e407e..3d1cba42 100644 --- a/src/gen/zod/index.ts +++ b/src/gen/zod/index.ts @@ -4,12 +4,14 @@ export * from "./boroughSchema"; export * from "./capitalCommitmentSchema"; export * from "./capitalProjectBudgetedSchema"; export * from "./capitalProjectCategorySchema"; +export * from "./capitalProjectPageSchema"; export * from "./capitalProjectSchema"; export * from "./cityCouncilDistrictSchema"; export * from "./communityDistrictSchema"; export * from "./errorSchema"; export * from "./findAgenciesSchema"; export * from "./findBoroughsSchema"; +export * from "./findCityCouncilDistrictsSchema"; export * from "./findCommunityDistrictsByBoroughIdSchema"; export * from "./findLandUsesSchema"; export * from "./findTaxLotByBblSchema"; diff --git a/src/schema/city-council-district.ts b/src/schema/city-council-district.ts index c00e4b8d..674e1c26 100644 --- a/src/schema/city-council-district.ts +++ b/src/schema/city-council-district.ts @@ -10,5 +10,5 @@ export const cityCouncilDistrict = pgTable("city_council_district", { }); export const cityCouncilDistrictEntitySchema = z.object({ - id: z.string(), + id: z.string().regex(new RegExp("^([0-9]{1,2})$")), }); diff --git a/test/city-council-district/city-council-district.e2e-spec.ts b/test/city-council-district/city-council-district.e2e-spec.ts new file mode 100644 index 00000000..f160eb15 --- /dev/null +++ b/test/city-council-district/city-council-district.e2e-spec.ts @@ -0,0 +1,57 @@ +import * as request from "supertest"; +import { INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { DataRetrievalException } from "src/exception"; +import { HttpName } from "src/filter"; +import { CityCouncilDistrictRepository } from "src/city-council-district/city-council-district.repository"; +import { CityCouncilDistrictRepositoryMock } from "./city-council-district.repository.mock"; +import { CityCouncilDistrictModule } from "src/city-council-district/city-council-district.module"; +import { findCityCouncilDistrictsQueryResponseSchema } from "src/gen"; + +describe("City Council District e2e", () => { + let app: INestApplication; + + const cityCouncilDistrictRepositoryMock = + new CityCouncilDistrictRepositoryMock(); + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CityCouncilDistrictModule], + }) + .overrideProvider(CityCouncilDistrictRepository) + .useValue(cityCouncilDistrictRepositoryMock) + .compile(); + app = moduleRef.createNestApplication(); + await app.init(); + }); + + describe("findCityCouncilDistricts", () => { + it("should 200 amd return all city council districts", async () => { + const response = await request(app.getHttpServer()) + .get(`/city-council-districts`) + .expect(200); + expect(() => + findCityCouncilDistrictsQueryResponseSchema.parse(response.body), + ).not.toThrow(); + }); + + it("should 500 when the database errors", async () => { + const dataRetrievalException = new DataRetrievalException(); + jest + .spyOn(cityCouncilDistrictRepositoryMock, "findMany") + .mockImplementation(() => { + throw dataRetrievalException; + }); + + const response = await request(app.getHttpServer()) + .get(`/city-council-districts`) + .expect(500); + expect(response.body.message).toBe(dataRetrievalException.message); + expect(response.body.error).toBe(HttpName.INTERNAL_SEVER_ERROR); + }); + + afterAll(async () => { + await app.close(); + }); + }); +}); diff --git a/test/city-council-district/city-council-district.repository.mock.ts b/test/city-council-district/city-council-district.repository.mock.ts new file mode 100644 index 00000000..172b2af5 --- /dev/null +++ b/test/city-council-district/city-council-district.repository.mock.ts @@ -0,0 +1,10 @@ +import { findManyRepoSchema } from "src/city-council-district/city-council-district.repository.schema"; +import { generateMock } from "@anatine/zod-mock"; + +export class CityCouncilDistrictRepositoryMock { + findManyMocks = generateMock(findManyRepoSchema); + + async findMany() { + return this.findManyMocks; + } +}