From 0c501f7bb407301ef9eecc70ff37dd84bba0f3fb Mon Sep 17 00:00:00 2001 From: David Hochbaum Date: Wed, 22 May 2024 09:14:40 -0400 Subject: [PATCH] OpenAPI Implementation findCommunityDistrictsByBoroughId endpoint implement find community districts by borough id endpoint - cd by id controller, module, repository, service - added notNull contraints to community districts schema + migration - added unit and e2e tests --- db/migration/0007_wet_morlun.sql | 2 + db/migration/meta/0007_snapshot.json | 974 +++++++++++++++++++++++ db/migration/meta/_journal.json | 7 + openapi/openapi.yaml | 2 +- package-lock.json | 28 +- src/borough/borough.controller.ts | 39 +- src/borough/borough.repository.schema.ts | 16 +- src/borough/borough.repository.ts | 43 +- src/borough/borough.service.spec.ts | 45 +- src/borough/borough.service.ts | 13 + src/schema/community-district.ts | 10 +- test/borough/borough.e2e-spec.ts | 45 +- test/borough/borough.repository.mock.ts | 34 +- 13 files changed, 1235 insertions(+), 23 deletions(-) create mode 100644 db/migration/0007_wet_morlun.sql create mode 100644 db/migration/meta/0007_snapshot.json diff --git a/db/migration/0007_wet_morlun.sql b/db/migration/0007_wet_morlun.sql new file mode 100644 index 00000000..b9e39b93 --- /dev/null +++ b/db/migration/0007_wet_morlun.sql @@ -0,0 +1,2 @@ +ALTER TABLE "community_district" ALTER COLUMN "borough_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "community_district" ALTER COLUMN "id" SET NOT NULL; \ No newline at end of file diff --git a/db/migration/meta/0007_snapshot.json b/db/migration/meta/0007_snapshot.json new file mode 100644 index 00000000..5f82acf1 --- /dev/null +++ b/db/migration/meta/0007_snapshot.json @@ -0,0 +1,974 @@ +{ + "id": "4cc50fec-be53-422a-a04a-7778eb02a9f2", + "prevId": "754b4da3-c860-46fe-84a8-b7ab20e3d33e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agency_budget": { + "name": "agency_budget", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sponsor": { + "name": "sponsor", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agency_budget_sponsor_agency_initials_fk": { + "name": "agency_budget_sponsor_agency_initials_fk", + "tableFrom": "agency_budget", + "tableTo": "agency", + "columnsFrom": [ + "sponsor" + ], + "columnsTo": [ + "initials" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.agency": { + "name": "agency", + "schema": "", + "columns": { + "initials": { + "name": "initials", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.borough": { + "name": "borough", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(1)", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbr": { + "name": "abbr", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.budget_line": { + "name": "budget_line", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "budget_line_code_agency_budget_code_fk": { + "name": "budget_line_code_agency_budget_code_fk", + "tableFrom": "budget_line", + "tableTo": "agency_budget", + "columnsFrom": [ + "code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "budget_line_code_id_pk": { + "name": "budget_line_code_id_pk", + "columns": [ + "code", + "id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.capital_commitment_fund": { + "name": "capital_commitment_fund", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "capital_commitment_id": { + "name": "capital_commitment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capital_fund_category": { + "name": "capital_fund_category", + "type": "capital_fund_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "capital_commitment_fund_capital_commitment_id_captial_commitment_id_fk": { + "name": "capital_commitment_fund_capital_commitment_id_captial_commitment_id_fk", + "tableFrom": "capital_commitment_fund", + "tableTo": "captial_commitment", + "columnsFrom": [ + "capital_commitment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.capital_commitment_type": { + "name": "capital_commitment_type", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "char(4)", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.captial_commitment": { + "name": "captial_commitment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "char(4)", + "primaryKey": false, + "notNull": false + }, + "planned_date": { + "name": "planned_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "managing_code": { + "name": "managing_code", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "capital_project_id": { + "name": "capital_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget_line_code": { + "name": "budget_line_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget_line_id": { + "name": "budget_line_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "captial_commitment_type_capital_commitment_type_code_fk": { + "name": "captial_commitment_type_capital_commitment_type_code_fk", + "tableFrom": "captial_commitment", + "tableTo": "capital_commitment_type", + "columnsFrom": [ + "type" + ], + "columnsTo": [ + "code" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "captial_commitment_managing_code_capital_project_id_capital_project_managing_code_id_fk": { + "name": "captial_commitment_managing_code_capital_project_id_capital_project_managing_code_id_fk", + "tableFrom": "captial_commitment", + "tableTo": "capital_project", + "columnsFrom": [ + "managing_code", + "capital_project_id" + ], + "columnsTo": [ + "managing_code", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "captial_commitment_budget_line_code_budget_line_id_budget_line_code_id_fk": { + "name": "captial_commitment_budget_line_code_budget_line_id_budget_line_code_id_fk", + "tableFrom": "captial_commitment", + "tableTo": "budget_line", + "columnsFrom": [ + "budget_line_code", + "budget_line_id" + ], + "columnsTo": [ + "code", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.capital_project_checkbook": { + "name": "capital_project_checkbook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "managing_code": { + "name": "managing_code", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "capital_project_id": { + "name": "capital_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_fk": { + "name": "custom_fk", + "tableFrom": "capital_project_checkbook", + "tableTo": "capital_project", + "columnsFrom": [ + "managing_code", + "capital_project_id" + ], + "columnsTo": [ + "managing_code", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.capital_project_fund": { + "name": "capital_project_fund", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "managing_code": { + "name": "managing_code", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "capital_project_id": { + "name": "capital_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capital_fund_category": { + "name": "capital_fund_category", + "type": "capital_fund_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "stage": { + "name": "stage", + "type": "capital_project_fund_stage", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "custom_fk": { + "name": "custom_fk", + "tableFrom": "capital_project_fund", + "tableTo": "capital_project", + "columnsFrom": [ + "managing_code", + "capital_project_id" + ], + "columnsTo": [ + "managing_code", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.capital_project": { + "name": "capital_project", + "schema": "", + "columns": { + "managing_code": { + "name": "managing_code", + "type": "char(3)", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managing_agency": { + "name": "managing_agency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_date": { + "name": "min_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "max_date": { + "name": "max_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "capital_project_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "li_ft_m_pnt": { + "name": "li_ft_m_pnt", + "type": "geometry(multiPoint,2263)", + "primaryKey": false, + "notNull": false + }, + "li_ft_m_poly": { + "name": "li_ft_m_poly", + "type": "geometry(multiPolygon,2263)", + "primaryKey": false, + "notNull": false + }, + "mercator_label": { + "name": "mercator_label", + "type": "geometry(point,3857)", + "primaryKey": false, + "notNull": false + }, + "mercator_fill_m_pnt": { + "name": "mercator_fill_m_pnt", + "type": "geometry(multiPoint,3857)", + "primaryKey": false, + "notNull": false + }, + "mercator_fill_m_poly": { + "name": "mercator_fill_m_poly", + "type": "geometry(multiPolygon,3857)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "capital_project_mercator_fill_m_poly_index": { + "name": "capital_project_mercator_fill_m_poly_index", + "columns": [ + { + "expression": "mercator_fill_m_poly", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "GIST", + "with": {} + }, + "capital_project_mercator_fill_m_pnt_index": { + "name": "capital_project_mercator_fill_m_pnt_index", + "columns": [ + { + "expression": "mercator_fill_m_pnt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "GIST", + "with": {} + } + }, + "foreignKeys": { + "capital_project_managing_code_managing_code_id_fk": { + "name": "capital_project_managing_code_managing_code_id_fk", + "tableFrom": "capital_project", + "tableTo": "managing_code", + "columnsFrom": [ + "managing_code" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "capital_project_managing_agency_agency_initials_fk": { + "name": "capital_project_managing_agency_agency_initials_fk", + "tableFrom": "capital_project", + "tableTo": "agency", + "columnsFrom": [ + "managing_agency" + ], + "columnsTo": [ + "initials" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "capital_project_managing_code_id_pk": { + "name": "capital_project_managing_code_id_pk", + "columns": [ + "managing_code", + "id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.city_council_district": { + "name": "city_council_district", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "li_ft": { + "name": "li_ft", + "type": "geometry(multiPolygon,2263)", + "primaryKey": false, + "notNull": false + }, + "mercator_fill": { + "name": "mercator_fill", + "type": "geometry(multiPolygon,3857)", + "primaryKey": false, + "notNull": false + }, + "mercator_label": { + "name": "mercator_label", + "type": "geometry(point,3857)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.community_district": { + "name": "community_district", + "schema": "", + "columns": { + "borough_id": { + "name": "borough_id", + "type": "char(1)", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "char(2)", + "primaryKey": false, + "notNull": true + }, + "li_ft": { + "name": "li_ft", + "type": "geometry(multiPoint,2263)", + "primaryKey": false, + "notNull": false + }, + "mercator_fill": { + "name": "mercator_fill", + "type": "geometry(multiPolygon,3857)", + "primaryKey": false, + "notNull": false + }, + "mercator_label": { + "name": "mercator_label", + "type": "geometry(point,3857)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "community_district_borough_id_borough_id_fk": { + "name": "community_district_borough_id_borough_id_fk", + "tableFrom": "community_district", + "tableTo": "borough", + "columnsFrom": [ + "borough_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "community_district_borough_id_id_pk": { + "name": "community_district_borough_id_id_pk", + "columns": [ + "borough_id", + "id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.land_use": { + "name": "land_use", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(2)", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "char(9)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.managing_code": { + "name": "managing_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(3)", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.tax_lot": { + "name": "tax_lot", + "schema": "", + "columns": { + "bbl": { + "name": "bbl", + "type": "char(10)", + "primaryKey": true, + "notNull": true + }, + "borough_id": { + "name": "borough_id", + "type": "char(1)", + "primaryKey": false, + "notNull": true + }, + "block": { + "name": "block", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lot": { + "name": "lot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "land_use_id": { + "name": "land_use_id", + "type": "char(2)", + "primaryKey": false, + "notNull": false + }, + "wgs84": { + "name": "wgs84", + "type": "geography(multiPolygon, 4326)", + "primaryKey": false, + "notNull": true + }, + "li_ft": { + "name": "li_ft", + "type": "geometry(multiPolygon,2263)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tax_lot_borough_id_borough_id_fk": { + "name": "tax_lot_borough_id_borough_id_fk", + "tableFrom": "tax_lot", + "tableTo": "borough", + "columnsFrom": [ + "borough_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tax_lot_land_use_id_land_use_id_fk": { + "name": "tax_lot_land_use_id_land_use_id_fk", + "tableFrom": "tax_lot", + "tableTo": "land_use", + "columnsFrom": [ + "land_use_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.zoning_district": { + "name": "zoning_district", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wgs84": { + "name": "wgs84", + "type": "geography(multiPolygon, 4326)", + "primaryKey": false, + "notNull": true + }, + "li_ft": { + "name": "li_ft", + "type": "geometry(multiPolygon,2263)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.zoning_district_class": { + "name": "zoning_district_class", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "category": { + "name": "category", + "type": "category", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "char(9)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.zoning_district_zoning_district_class": { + "name": "zoning_district_zoning_district_class", + "schema": "", + "columns": { + "zoning_district_id": { + "name": "zoning_district_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "zoning_district_class_id": { + "name": "zoning_district_class_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "zoning_district_zoning_district_class_zoning_district_id_zoning_district_id_fk": { + "name": "zoning_district_zoning_district_class_zoning_district_id_zoning_district_id_fk", + "tableFrom": "zoning_district_zoning_district_class", + "tableTo": "zoning_district", + "columnsFrom": [ + "zoning_district_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "zoning_district_zoning_district_class_zoning_district_class_id_zoning_district_class_id_fk": { + "name": "zoning_district_zoning_district_class_zoning_district_class_id_zoning_district_class_id_fk", + "tableFrom": "zoning_district_zoning_district_class", + "tableTo": "zoning_district_class", + "columnsFrom": [ + "zoning_district_class_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "public.capital_fund_category": { + "name": "capital_fund_category", + "schema": "public", + "values": [ + "city-non-exempt", + "city-exempt", + "city-cost", + "non-city-state", + "non-city-federal", + "non-city-other", + "non-city-cost", + "total" + ] + }, + "public.capital_project_fund_stage": { + "name": "capital_project_fund_stage", + "schema": "public", + "values": [ + "adopt", + "allocate", + "commit", + "spent" + ] + }, + "public.capital_project_category": { + "name": "capital_project_category", + "schema": "public", + "values": [ + "Fixed Asset", + "Lump Sum", + "ITT, Vehicles and Equipment" + ] + }, + "public.category": { + "name": "category", + "schema": "public", + "values": [ + "Residential", + "Commercial", + "Manufacturing" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/db/migration/meta/_journal.json b/db/migration/meta/_journal.json index 73c1ac3c..fa9d8e4e 100644 --- a/db/migration/meta/_journal.json +++ b/db/migration/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1717534239478, "tag": "0006_brainy_wong", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1717765658928, + "tag": "0007_wet_morlun", + "breakpoints": true } ] } \ No newline at end of file diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index ded169aa..1dc3083e 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -63,7 +63,7 @@ paths: $ref: "#/components/responses/InternalServerError" /boroughs/{boroughId}/community-districts: get: - summary: 🚧 Find community districts within a borough + summary: Find community districts within a borough operationId: findCommunityDistrictsByBoroughId tags: - Community Districts diff --git a/package-lock.json b/package-lock.json index 350220b1..d2bfef54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6810,14 +6810,15 @@ "dev": true }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -7184,6 +7185,27 @@ "node": ">=6" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", diff --git a/src/borough/borough.controller.ts b/src/borough/borough.controller.ts index a35f78a4..76d66fdd 100644 --- a/src/borough/borough.controller.ts +++ b/src/borough/borough.controller.ts @@ -1,8 +1,29 @@ -import { Controller, Get, UseFilters } from "@nestjs/common"; +import { + Controller, + Get, + Injectable, + Param, + UseFilters, + UsePipes, +} from "@nestjs/common"; import { BoroughService } from "./borough.service"; -import { InternalServerErrorExceptionFilter } from "src/filter"; +import { ZodValidationPipe } from "src/pipes/zod-validation-pipe"; +import { + FindCommunityDistrictsByBoroughIdPathParams, + findCommunityDistrictsByBoroughIdPathParamsSchema, +} from "src/gen"; +import { + BadRequestExceptionFilter, + InternalServerErrorExceptionFilter, + NotFoundExceptionFilter, +} from "src/filter"; -@UseFilters(InternalServerErrorExceptionFilter) +@Injectable() +@UseFilters( + BadRequestExceptionFilter, + InternalServerErrorExceptionFilter, + NotFoundExceptionFilter, +) @Controller("boroughs") export class BoroughController { constructor(private readonly boroughService: BoroughService) {} @@ -11,4 +32,16 @@ export class BoroughController { async findMany() { return this.boroughService.findMany(); } + + @Get("/:boroughId/community-districts") + @UsePipes( + new ZodValidationPipe(findCommunityDistrictsByBoroughIdPathParamsSchema), + ) + async findCommunityDistrictsById( + @Param() params: FindCommunityDistrictsByBoroughIdPathParams, + ) { + return this.boroughService.findCommunityDistrictsByBoroughId( + params.boroughId, + ); + } } diff --git a/src/borough/borough.repository.schema.ts b/src/borough/borough.repository.schema.ts index ebe935b0..25d63f0e 100644 --- a/src/borough/borough.repository.schema.ts +++ b/src/borough/borough.repository.schema.ts @@ -1,6 +1,20 @@ -import { boroughEntitySchema } from "src/schema"; +import { boroughEntitySchema, communityDistrictEntitySchema } from "src/schema"; import { z } from "zod"; export const findManyRepoSchema = z.array(boroughEntitySchema); export type FindManyRepo = z.infer; + +export const checkByIdRepoSchema = boroughEntitySchema.pick({ + id: true, +}); + +export type CheckByIdRepo = z.infer; + +export const findCommunityDistrictsByBoroughIdRepoSchema = z.array( + communityDistrictEntitySchema, +); + +export type FindCommunityDistrictsByBoroughIdRepo = z.infer< + typeof findCommunityDistrictsByBoroughIdRepoSchema +>; diff --git a/src/borough/borough.repository.ts b/src/borough/borough.repository.ts index a20675c6..bd1f5a5e 100644 --- a/src/borough/borough.repository.ts +++ b/src/borough/borough.repository.ts @@ -1,7 +1,13 @@ import { Inject } from "@nestjs/common"; import { DB, DbType } from "src/global/providers/db.provider"; import { DataRetrievalException } from "src/exception"; -import { FindManyRepo } from "./borough.repository.schema"; +import { + CheckByIdRepo, + FindManyRepo, + FindCommunityDistrictsByBoroughIdRepo, +} from "./borough.repository.schema"; +import { communityDistrict } from "src/schema"; +import { eq } from "drizzle-orm"; export class BoroughRepository { constructor( @@ -9,6 +15,25 @@ export class BoroughRepository { private readonly db: DbType, ) {} + #checkBoroughById = this.db.query.borough + .findFirst({ + columns: { + id: true, + }, + where: (borough, { eq, sql }) => eq(borough.id, sql.placeholder("id")), + }) + .prepare("checkBoroughById"); + + async checkBoroughById(id: string): Promise { + try { + return await this.#checkBoroughById.execute({ + id, + }); + } catch { + throw new DataRetrievalException(); + } + } + async findMany(): Promise { try { return await this.db.query.borough.findMany(); @@ -16,4 +41,20 @@ export class BoroughRepository { throw new DataRetrievalException(); } } + + async findCommunityDistrictsByBoroughId( + id: string, + ): Promise { + try { + return await this.db.query.communityDistrict.findMany({ + columns: { + id: true, + boroughId: true, + }, + where: eq(communityDistrict.boroughId, id), + }); + } catch { + throw new DataRetrievalException(); + } + } } diff --git a/src/borough/borough.service.spec.ts b/src/borough/borough.service.spec.ts index ac081cf6..a750285b 100644 --- a/src/borough/borough.service.spec.ts +++ b/src/borough/borough.service.spec.ts @@ -1,15 +1,19 @@ -import { BoroughRepositoryMock } from "../../test/borough/borough.repository.mock"; -import { Test } from "@nestjs/testing"; import { BoroughRepository } from "src/borough/borough.repository"; -import { findBoroughsQueryResponseSchema } from "src/gen"; import { BoroughService } from "./borough.service"; +import { BoroughRepositoryMock } from "../../test/borough/borough.repository.mock"; +import { Test } from "@nestjs/testing"; +import { + findBoroughsQueryResponseSchema, + findCommunityDistrictsByBoroughIdQueryResponseSchema, +} from "src/gen"; +import { ResourceNotFoundException } from "src/exception"; describe("Borough service unit", () => { let boroughService: BoroughService; - beforeEach(async () => { - const boroughRepositoryMock = new BoroughRepositoryMock(); + const boroughRepositoryMock = new BoroughRepositoryMock(); + beforeEach(async () => { const moduleRef = await Test.createTestingModule({ providers: [BoroughService, BoroughRepository], }) @@ -20,8 +24,33 @@ describe("Borough service unit", () => { boroughService = moduleRef.get(BoroughService); }); - it("service should return a findBoroughsQueryResponseSchema compliant object", async () => { - const boroughs = await boroughService.findMany(); - expect(() => findBoroughsQueryResponseSchema.parse(boroughs)).not.toThrow(); + describe("findMany", () => { + it("service should return a findBoroughsQueryResponseSchema compliant object", async () => { + const boroughs = await boroughService.findMany(); + expect(() => + findBoroughsQueryResponseSchema.parse(boroughs), + ).not.toThrow(); + }); + }); + + describe("findCommunityDistrictsByBoroughId", () => { + it("service should return a community districts compliant object", async () => { + const { id } = boroughRepositoryMock.checkBoroughByIdMocks[0]; + const communityDistricts = + await boroughService.findCommunityDistrictsByBoroughId(id); + + expect(() => + findCommunityDistrictsByBoroughIdQueryResponseSchema.parse( + communityDistricts, + ), + ).not.toThrow(); + }); + + it("service should throw a resource error when requesting with a missing id", async () => { + const missingId = ""; + const zoningDistrict = + boroughService.findCommunityDistrictsByBoroughId(missingId); + expect(zoningDistrict).rejects.toThrow(ResourceNotFoundException); + }); }); }); diff --git a/src/borough/borough.service.ts b/src/borough/borough.service.ts index f0d7886d..1b1c4cdb 100644 --- a/src/borough/borough.service.ts +++ b/src/borough/borough.service.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from "@nestjs/common"; import { BoroughRepository } from "./borough.repository"; +import { ResourceNotFoundException } from "src/exception"; @Injectable() export class BoroughService { @@ -14,4 +15,16 @@ export class BoroughService { boroughs, }; } + + async findCommunityDistrictsByBoroughId(id: string) { + const boroughCheck = await this.boroughRepository.checkBoroughById(id); + if (boroughCheck === undefined) throw new ResourceNotFoundException(); + + const communityDistricts = + await this.boroughRepository.findCommunityDistrictsByBoroughId(id); + + return { + communityDistricts, + }; + } } diff --git a/src/schema/community-district.ts b/src/schema/community-district.ts index 68155d7a..522c305a 100644 --- a/src/schema/community-district.ts +++ b/src/schema/community-district.ts @@ -6,8 +6,10 @@ import { z } from "zod"; export const communityDistrict = pgTable( "community_district", { - boroughId: char("borough_id", { length: 1 }).references(() => borough.id), - id: char("id", { length: 2 }), + boroughId: char("borough_id", { length: 1 }) + .notNull() + .references(() => borough.id), + id: char("id", { length: 2 }).notNull(), liFt: multiPointGeom("li_ft", 2263), mercatorFill: multiPolygonGeom("mercator_fill", 3857), mercatorLabel: pointGeom("mercator_label", 3857), @@ -20,6 +22,6 @@ export const communityDistrict = pgTable( ); export const communityDistrictEntitySchema = z.object({ - boroughId: z.string().length(1), - id: z.string().length(2), + boroughId: z.string().length(1).regex(new RegExp("[1-9]")), + id: z.string().length(2).regex(new RegExp("^([0-9]{2})$")), }); diff --git a/test/borough/borough.e2e-spec.ts b/test/borough/borough.e2e-spec.ts index 534619b3..07879d8f 100644 --- a/test/borough/borough.e2e-spec.ts +++ b/test/borough/borough.e2e-spec.ts @@ -4,7 +4,10 @@ import { Test } from "@nestjs/testing"; import { BoroughRepository } from "src/borough/borough.repository"; import { BoroughRepositoryMock } from "./borough.repository.mock"; import { BoroughModule } from "src/borough/borough.module"; -import { findBoroughsQueryResponseSchema } from "src/gen"; +import { + findBoroughsQueryResponseSchema, + findCommunityDistrictsByBoroughIdQueryResponseSchema, +} from "src/gen"; import { DataRetrievalException } from "src/exception"; import { HttpName } from "src/filter"; @@ -50,6 +53,46 @@ describe("Borough e2e", () => { }); }); + describe("findCommunityDistrictsByBoroughId", () => { + it("should 200 and return community districts for a given borough id", async () => { + const mock = boroughRepositoryMock.checkBoroughByIdMocks[0]; + + const response = await request(app.getHttpServer()) + .get(`/boroughs/${mock.id}/community-districts`) + .expect(200); + + expect(() => { + findCommunityDistrictsByBoroughIdQueryResponseSchema.parse( + response.body, + ); + }).not.toThrow(); + }); + + it("should 400 and when finding by an invalid id", async () => { + const invalidId = "MN"; + const response = await request(app.getHttpServer()) + .get(`/boroughs/${invalidId}/community-districts`) + .expect(400); + expect(response.body.error).toBe(HttpName.BAD_REQUEST); + }); + + it("should 500 when the database errors", async () => { + const dataRetrievalException = new DataRetrievalException(); + jest + .spyOn(boroughRepositoryMock, "checkBoroughById") + .mockImplementationOnce(() => { + throw dataRetrievalException; + }); + + const mock = boroughRepositoryMock.checkBoroughByIdMocks[0]; + const response = await request(app.getHttpServer()) + .get(`/boroughs/${mock.id}/community-districts`) + .expect(500); + expect(response.body.error).toBe(HttpName.INTERNAL_SEVER_ERROR); + expect(response.body.message).toBe(dataRetrievalException.message); + }); + }); + afterAll(async () => { await app.close(); }); diff --git a/test/borough/borough.repository.mock.ts b/test/borough/borough.repository.mock.ts index d6afa592..81e5c06b 100644 --- a/test/borough/borough.repository.mock.ts +++ b/test/borough/borough.repository.mock.ts @@ -1,10 +1,42 @@ -import { findManyRepoSchema } from "src/borough/borough.repository.schema"; +import { + findManyRepoSchema, + checkByIdRepoSchema, + findCommunityDistrictsByBoroughIdRepoSchema, +} from "src/borough/borough.repository.schema"; import { generateMock } from "@anatine/zod-mock"; export class BoroughRepositoryMock { + numberOfMocks = 1; + + checkBoroughByIdMocks = Array.from(Array(this.numberOfMocks), (_, seed) => + generateMock(checkByIdRepoSchema, { seed: seed + 1 }), + ); + + async checkBoroughById(id: string) { + return this.checkBoroughByIdMocks.find((row) => row.id === id); + } + findManyMocks = generateMock(findManyRepoSchema); async findMany() { return this.findManyMocks; } + + findCommunityDistrictsByBoroughIdMocks = this.checkBoroughByIdMocks.map( + (checkCommunityDistrict) => { + return { + [checkCommunityDistrict.id]: generateMock( + findCommunityDistrictsByBoroughIdRepoSchema, + ), + }; + }, + ); + + async findCommunityDistrictsByBoroughId(id: string) { + const results = this.findCommunityDistrictsByBoroughIdMocks.find( + (communityDistricts) => id in communityDistricts, + ); + + return results === undefined ? [] : results[id]; + } }