From 49dd8241fa67b8b57e0bb38b71a742b59e9f31d7 Mon Sep 17 00:00:00 2001 From: tangoyankee Date: Fri, 14 Jun 2024 14:36:34 -0400 Subject: [PATCH] Community district tiles Serve mvts for community districts closes #252 --- db/migration/0008_closed_smasher.sql | 2 + db/migration/meta/0008_snapshot.json | 1005 +++++++++++++++++ db/migration/meta/_journal.json | 7 + src/app.module.ts | 2 + .../community-district.controller.ts | 42 + .../community-district.module.ts | 11 + .../community-district.repository.schema.ts | 6 + .../community-district.repository.ts | 78 ++ .../community-district.service.spec.ts | 37 + .../community-district.service.ts | 14 + src/schema/community-district.ts | 4 +- .../community-district.e2e-spec.ts | 79 ++ .../community-district.repository.mock.ts | 21 + 13 files changed, 1307 insertions(+), 1 deletion(-) create mode 100644 db/migration/0008_closed_smasher.sql create mode 100644 db/migration/meta/0008_snapshot.json create mode 100644 src/community-district/community-district.controller.ts create mode 100644 src/community-district/community-district.module.ts create mode 100644 src/community-district/community-district.repository.schema.ts create mode 100644 src/community-district/community-district.repository.ts create mode 100644 src/community-district/community-district.service.spec.ts create mode 100644 src/community-district/community-district.service.ts create mode 100644 test/community-district/community-district.e2e-spec.ts create mode 100644 test/community-district/community-district.repository.mock.ts diff --git a/db/migration/0008_closed_smasher.sql b/db/migration/0008_closed_smasher.sql new file mode 100644 index 00000000..5bf456ef --- /dev/null +++ b/db/migration/0008_closed_smasher.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS "community_district_mercator_fill_index" ON "community_district" USING GIST (mercator_fill);--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "community_district_mercator_label_index" ON "community_district" USING GIST (mercator_label); \ No newline at end of file diff --git a/db/migration/meta/0008_snapshot.json b/db/migration/meta/0008_snapshot.json new file mode 100644 index 00000000..347c9d47 --- /dev/null +++ b/db/migration/meta/0008_snapshot.json @@ -0,0 +1,1005 @@ +{ + "id": "6f2c31be-802b-4134-afec-beecfb430034", + "prevId": "4cc50fec-be53-422a-a04a-7778eb02a9f2", + "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": { + "community_district_mercator_fill_index": { + "name": "community_district_mercator_fill_index", + "columns": [ + { + "expression": "mercator_fill", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "GIST", + "with": {} + }, + "community_district_mercator_label_index": { + "name": "community_district_mercator_label_index", + "columns": [ + { + "expression": "mercator_label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "GIST", + "with": {} + } + }, + "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 fa9d8e4e..f5d43dd0 100644 --- a/db/migration/meta/_journal.json +++ b/db/migration/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1717765658928, "tag": "0007_wet_morlun", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1718390401153, + "tag": "0008_closed_smasher", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index d0a427a3..66d3f572 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ import { ZoningDistrictClassModule } from "./zoning-district-class/zoning-distri import { AgencyModule } from "./agency/agency.module"; import { CityCouncilDistrictModule } from "./city-council-district/city-council-district.module"; import { CapitalProjectModule } from "./capital-project/capital-project.module"; +import { CommunityDistrictModule } from "./community-district/community-district.module"; @Module({ imports: [ @@ -43,6 +44,7 @@ import { CapitalProjectModule } from "./capital-project/capital-project.module"; BoroughModule, CityCouncilDistrictModule, CapitalProjectModule, + CommunityDistrictModule, LandUseModule, TaxLotModule, ZoningDistrictModule, diff --git a/src/community-district/community-district.controller.ts b/src/community-district/community-district.controller.ts new file mode 100644 index 00000000..a6032663 --- /dev/null +++ b/src/community-district/community-district.controller.ts @@ -0,0 +1,42 @@ +import { + Controller, + Get, + Param, + Res, + UseFilters, + UsePipes, +} from "@nestjs/common"; +import { Response } from "express"; +import { CommunityDistrictService } from "./community-district.service"; +import { + BadRequestExceptionFilter, + InternalServerErrorExceptionFilter, +} from "src/filter"; +import { + FindCommunityDistrictTilesPathParams, + findCommunityDistrictTilesPathParamsSchema, +} from "../gen"; +import { DecodeParamsPipe } from "src/pipes/decode-params-pipe"; +import { ZodValidationPipe } from "src/pipes/zod-validation-pipe"; + +@UseFilters(BadRequestExceptionFilter, InternalServerErrorExceptionFilter) +@Controller("community-districts") +export class CommunityDistrictController { + constructor( + private readonly communityDistrictService: CommunityDistrictService, + ) {} + + @UsePipes( + new DecodeParamsPipe(findCommunityDistrictTilesPathParamsSchema), + new ZodValidationPipe(findCommunityDistrictTilesPathParamsSchema), + ) + @Get("/:z/:x/:y.pbf") + async findTiles( + @Param() params: FindCommunityDistrictTilesPathParams, + @Res() res: Response, + ) { + const tile = await this.communityDistrictService.findTiles(params); + res.set("Content-Type", "application/x-protobuf"); + res.send(tile); + } +} diff --git a/src/community-district/community-district.module.ts b/src/community-district/community-district.module.ts new file mode 100644 index 00000000..5e57559b --- /dev/null +++ b/src/community-district/community-district.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { CommunityDistrictController } from "./community-district.controller"; +import { CommunityDistrictService } from "./community-district.service"; +import { CommunityDistrictRepository } from "./community-district.repository"; + +@Module({ + exports: [CommunityDistrictService], + providers: [CommunityDistrictService, CommunityDistrictRepository], + controllers: [CommunityDistrictController], +}) +export class CommunityDistrictModule {} diff --git a/src/community-district/community-district.repository.schema.ts b/src/community-district/community-district.repository.schema.ts new file mode 100644 index 00000000..38d50218 --- /dev/null +++ b/src/community-district/community-district.repository.schema.ts @@ -0,0 +1,6 @@ +import { mvtEntitySchema } from "src/schema/mvt"; +import { z } from "zod"; + +export const findTilesRepoSchema = mvtEntitySchema; + +export type FindTilesRepo = z.infer; diff --git a/src/community-district/community-district.repository.ts b/src/community-district/community-district.repository.ts new file mode 100644 index 00000000..afa985f0 --- /dev/null +++ b/src/community-district/community-district.repository.ts @@ -0,0 +1,78 @@ +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 { borough, communityDistrict } from "src/schema"; +import { sql, isNotNull, eq } from "drizzle-orm"; +import { DataRetrievalException } from "src/exception"; + +export class CommunityDistrictRepository { + constructor( + @Inject(DB) + private readonly db: DbType, + ) {} + + async findTiles( + params: FindCommunityDistrictTilesPathParams, + ): Promise { + const { z, x, y } = params; + + try { + const tileFill = this.db + .select({ + boroughIdCommunityDistrictId: + sql`${communityDistrict.boroughId}||${communityDistrict.id}`.as( + "boroughIdCommunityDistrictId", + ), + geomFill: sql`ST_AsMVTGeom( + ${communityDistrict.mercatorFill}, + ST_TileEnvelope(${z}, ${x}, ${y}), + 4096, 64, true)`.as("geomFill"), + }) + .from(communityDistrict) + .where( + sql`${communityDistrict.mercatorFill} && ST_TileEnvelope(${z},${x},${y})`, + ) + .as("tile"); + + const dataFill = await this.db + .select({ + mvt: sql`ST_AsMVT(tile, 'community-district-fill', 4096, 'geomFill')`, + }) + .from(tileFill) + .where(isNotNull(tileFill.geomFill)); + + const tileLabel = this.db + .select({ + boroughIdCommunityDistrictId: + sql`${communityDistrict.boroughId}||${communityDistrict.id}`.as( + "boroughIdCommunityDistrictId", + ), + boroughAbbr: borough.abbr, + geomLabel: sql`ST_AsMVTGeom( + ${communityDistrict.mercatorLabel}, + ST_TileEnvelope(${z}, ${x}, ${y}), + 4096, 64, true)`.as("geomLabel"), + }) + .from(communityDistrict) + .leftJoin(borough, eq(communityDistrict.boroughId, borough.id)) + .where( + sql`${communityDistrict.mercatorLabel} && ST_TileEnvelope(${z},${x},${y})`, + ) + .as("tile"); + + const dataLabel = this.db + .select({ + mvt: sql`ST_AsMVT(tile, 'community-district-label', 4096, 'geomLabel')`, + }) + .from(tileLabel) + .where(isNotNull(tileLabel.geomLabel)); + + const [fill, label] = await Promise.all([dataFill, dataLabel]); + + return Buffer.concat([fill[0].mvt, label[0].mvt] as Uint8Array[]); + } catch { + throw new DataRetrievalException(); + } + } +} diff --git a/src/community-district/community-district.service.spec.ts b/src/community-district/community-district.service.spec.ts new file mode 100644 index 00000000..caac5c19 --- /dev/null +++ b/src/community-district/community-district.service.spec.ts @@ -0,0 +1,37 @@ +import { CommunityDistrictRepositoryMock } from "test/community-district/community-district.repository.mock"; +import { CommunityDistrictService } from "./community-district.service"; +import { Test } from "@nestjs/testing"; +import { CommunityDistrictRepository } from "./community-district.repository"; +import { findCommunityDistrictTilesQueryResponseSchema } from "src/gen"; + +describe("CommunityDistrictService", () => { + let communityDistrictService: CommunityDistrictService; + + const communityDistrictRepository = new CommunityDistrictRepositoryMock(); + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [CommunityDistrictService, CommunityDistrictRepository], + }) + .overrideProvider(CommunityDistrictRepository) + .useValue(communityDistrictRepository) + .compile(); + + communityDistrictService = moduleRef.get( + CommunityDistrictService, + ); + }); + + describe("findTiles", () => { + it("should return an mvt when requesting coordinates", async () => { + const mvt = await communityDistrictService.findTiles({ + z: 1, + x: 1, + y: 1, + }); + expect(() => + findCommunityDistrictTilesQueryResponseSchema.parse(mvt).not.toThrow(), + ); + }); + }); +}); diff --git a/src/community-district/community-district.service.ts b/src/community-district/community-district.service.ts new file mode 100644 index 00000000..26d64dd3 --- /dev/null +++ b/src/community-district/community-district.service.ts @@ -0,0 +1,14 @@ +import { Inject } from "@nestjs/common"; +import { CommunityDistrictRepository } from "./community-district.repository"; +import { FindCommunityDistrictTilesPathParams } from "src/gen"; + +export class CommunityDistrictService { + constructor( + @Inject(CommunityDistrictRepository) + private readonly communityDistrictRepository: CommunityDistrictRepository, + ) {} + + async findTiles(params: FindCommunityDistrictTilesPathParams) { + return await this.communityDistrictRepository.findTiles(params); + } +} diff --git a/src/schema/community-district.ts b/src/schema/community-district.ts index d07df125..5a467fd9 100644 --- a/src/schema/community-district.ts +++ b/src/schema/community-district.ts @@ -1,4 +1,4 @@ -import { char, pgTable, primaryKey } from "drizzle-orm/pg-core"; +import { char, index, pgTable, primaryKey } from "drizzle-orm/pg-core"; import { borough } from "./borough"; import { multiPointGeom, multiPolygonGeom, pointGeom } from "src/drizzle-pgis"; import { z } from "zod"; @@ -17,6 +17,8 @@ export const communityDistrict = pgTable( (table) => { return { pk: primaryKey({ columns: [table.boroughId, table.id] }), + mercatorFillGix: index().using("GIST", table.mercatorFill), + mercatorLabelGix: index().using("GIST", table.mercatorLabel), }; }, ); diff --git a/test/community-district/community-district.e2e-spec.ts b/test/community-district/community-district.e2e-spec.ts new file mode 100644 index 00000000..1f17e75e --- /dev/null +++ b/test/community-district/community-district.e2e-spec.ts @@ -0,0 +1,79 @@ +import { INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { CommunityDistrictModule } from "src/community-district/community-district.module"; +import { CommunityDistrictRepository } from "src/community-district/community-district.repository"; +import { CommunityDistrictRepositoryMock } from "./community-district.repository.mock"; +import * as request from "supertest"; +import { HttpName } from "src/filter"; +import { + DataRetrievalException, + InvalidRequestParameterException, +} from "src/exception"; + +describe("Community Districts", () => { + let app: INestApplication; + + const communityDistrictRepository = new CommunityDistrictRepositoryMock(); + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [CommunityDistrictModule], + }) + .overrideProvider(CommunityDistrictRepository) + .useValue(communityDistrictRepository) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("findFills", () => { + it("should return pbf files when passing valid viewport", async () => { + const z = 1; + const x = 100; + const y = 200; + await request(app.getHttpServer()) + .get(`/community-districts/${z}/${x}/${y}.pbf`) + .expect("Content-Type", "application/x-protobuf") + .expect(200); + }); + + it("should 400 when finding a lettered viewport", async () => { + const z = "foo"; + const x = "bar"; + const y = "baz"; + + const response = await request(app.getHttpServer()) + .get(`/community-districts/${z}/${x}/${y}.pbf`) + .expect(400); + + expect(response.body.message).toBe( + new InvalidRequestParameterException().message, + ); + + expect(response.body.error).toBe(HttpName.BAD_REQUEST); + }); + + it("should 500 when there is a data retrieval error", async () => { + const dataRetrievalException = new DataRetrievalException(); + jest + .spyOn(communityDistrictRepository, "findTiles") + .mockImplementationOnce(() => { + throw dataRetrievalException; + }); + + const z = 1; + const x = 100; + const y = 200; + + const response = await request(app.getHttpServer()) + .get(`/community-districts/${z}/${x}/${y}.pbf`) + .expect(500); + expect(response.body.message).toBe(dataRetrievalException.message); + expect(response.body.error).toBe(HttpName.INTERNAL_SEVER_ERROR); + }); + }); +}); diff --git a/test/community-district/community-district.repository.mock.ts b/test/community-district/community-district.repository.mock.ts new file mode 100644 index 00000000..89c07eac --- /dev/null +++ b/test/community-district/community-district.repository.mock.ts @@ -0,0 +1,21 @@ +import { generateMock } from "@anatine/zod-mock"; +import { findTilesRepoSchema } from "src/community-district/community-district.repository.schema"; + +export class CommunityDistrictRepositoryMock { + findTilesMock = generateMock(findTilesRepoSchema); + + /** + * The database will always return tiles, + * even when the view is outside the extents. + * These would merely be empty tiles. + * + * To reflect this behavior in the mock, + * we disregard any viewport parameters and + * always return something. + * + * This applies to all mvt-related mocks + */ + async findTiles() { + return this.findTilesMock; + } +}