From ee6ce8873b7ce9b2ea5fcdcad2c60ec147580f21 Mon Sep 17 00:00:00 2001 From: Eric McGarry <46828798+mcgarrye@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:18:40 -0400 Subject: [PATCH] feat: map marker endpoint (#4453) * feat: map marker endpoint * feat: unit test * feat: integration tests * feat: correct unit test * feat: style corrections --- api/src/controllers/listing.controller.ts | 46 +++++---- .../dtos/listings/listing-map-marker.dto.ts | 22 +++++ api/src/services/listing.service.ts | 23 +++++ api/test/integration/listing.e2e-spec.ts | 32 +++++- .../permission-as-admin.e2e-spec.ts | 8 ++ ...n-as-juris-admin-correct-juris.e2e-spec.ts | 8 ++ ...ion-as-juris-admin-wrong-juris.e2e-spec.ts | 8 ++ .../permission-as-no-user.e2e-spec.ts | 8 ++ ...ion-as-partner-correct-listing.e2e-spec.ts | 8 ++ ...ssion-as-partner-wrong-listing.e2e-spec.ts | 8 ++ .../permission-as-public.e2e-spec.ts | 8 ++ .../unit/services/listing.service.spec.ts | 23 +++++ shared-helpers/src/types/backend-swagger.ts | 97 ++++++++++++------- 13 files changed, 241 insertions(+), 58 deletions(-) create mode 100644 api/src/dtos/listings/listing-map-marker.dto.ts diff --git a/api/src/controllers/listing.controller.ts b/api/src/controllers/listing.controller.ts index a64f2d580a..e3c2b4ea4f 100644 --- a/api/src/controllers/listing.controller.ts +++ b/api/src/controllers/listing.controller.ts @@ -35,6 +35,7 @@ import { ListingCreate } from '../dtos/listings/listing-create.dto'; import { ListingDuplicate } from '../dtos/listings/listing-duplicate.dto'; import { ListingCsvQueryParams } from '../dtos/listings/listing-csv-query-params.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; +import { ListingMapMarker } from '../dtos/listings/listing-map-marker.dto'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; import { ListingsRetrieveParams } from '../dtos/listings/listings-retrieve-params.dto'; import { ListingUpdate } from '../dtos/listings/listing-update.dto'; @@ -104,6 +105,16 @@ export class ListingController { return await this.listingCsvExportService.exportFile(req, res, queryParams); } + @Get('mapMarkers') + @ApiOperation({ + summary: 'Get listing map markers', + operationId: 'mapMarkers', + }) + @ApiOkResponse({ type: ListingMapMarker, isArray: true }) + async mapMarkers() { + return await this.listingService.mapMarkers(); + } + @Get(`external/:id`) @ApiOperation({ summary: 'Get listing for external consumption by id', @@ -124,23 +135,6 @@ export class ListingController { ); } - @Get(`:id`) - @ApiOperation({ summary: 'Get listing by id', operationId: 'retrieve' }) - @UseInterceptors(ClassSerializerInterceptor) - @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) - @ApiOkResponse({ type: Listing }) - async retrieve( - @Headers('language') language: LanguagesEnum, - @Param('id', new ParseUUIDPipe({ version: '4' })) listingId: string, - @Query() queryParams: ListingsRetrieveParams, - ) { - return await this.listingService.findOne( - listingId, - language, - queryParams.view, - ); - } - @Post() @ApiOperation({ summary: 'Create listing', operationId: 'create' }) @UseInterceptors(ClassSerializerInterceptor) @@ -220,4 +214,22 @@ export class ListingController { multiselectQuestionId, ); } + + // NestJS best practice to have get(':id') at the bottom of the file + @Get(`:id`) + @ApiOperation({ summary: 'Get listing by id', operationId: 'retrieve' }) + @UseInterceptors(ClassSerializerInterceptor) + @UsePipes(new ValidationPipe(defaultValidationPipeOptions)) + @ApiOkResponse({ type: Listing }) + async retrieve( + @Headers('language') language: LanguagesEnum, + @Param('id', new ParseUUIDPipe({ version: '4' })) listingId: string, + @Query() queryParams: ListingsRetrieveParams, + ) { + return await this.listingService.findOne( + listingId, + language, + queryParams.view, + ); + } } diff --git a/api/src/dtos/listings/listing-map-marker.dto.ts b/api/src/dtos/listings/listing-map-marker.dto.ts new file mode 100644 index 0000000000..099cfe1631 --- /dev/null +++ b/api/src/dtos/listings/listing-map-marker.dto.ts @@ -0,0 +1,22 @@ +import { Expose } from 'class-transformer'; +import { IsNumber, IsString, IsUUID } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; + +export class ListingMapMarker { + @Expose() + @IsString({ groups: [ValidationsGroupsEnum.default] }) + @IsUUID(4, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + id: string; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lat: number; + + @Expose() + @IsNumber({}, { groups: [ValidationsGroupsEnum.default] }) + @ApiProperty() + lng: number; +} diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts index 64c706a0bc..83ee4a265c 100644 --- a/api/src/services/listing.service.ts +++ b/api/src/services/listing.service.ts @@ -30,6 +30,7 @@ import { AmiChart } from '../dtos/ami-charts/ami-chart.dto'; import { Listing } from '../dtos/listings/listing.dto'; import { ListingCreate } from '../dtos/listings/listing-create.dto'; import { ListingDuplicate } from '../dtos/listings/listing-duplicate.dto'; +import { ListingMapMarker } from '../dtos/listings/listing-map-marker.dto'; import { ListingFilterParams } from '../dtos/listings/listings-filter-params.dto'; import { ListingsQueryParams } from '../dtos/listings/listings-query-params.dto'; import { ListingUpdate } from '../dtos/listings/listing-update.dto'; @@ -1840,4 +1841,26 @@ export class ListingService implements OnModuleInit { return listing.jurisdictionId; } + + async mapMarkers(): Promise { + const listingsRaw = await this.prisma.listings.findMany({ + select: { + id: true, + listingsBuildingAddress: true, + }, + where: { + status: ListingsStatusEnum.active, + }, + }); + + const listings = mapTo(Listing, listingsRaw); + + return listings.map((listing) => { + return { + id: listing.id, + lat: listing.listingsBuildingAddress.latitude, + lng: listing.listingsBuildingAddress.longitude, + } as ListingMapMarker; + }); + } } diff --git a/api/test/integration/listing.e2e-spec.ts b/api/test/integration/listing.e2e-spec.ts index 5397b5f827..9fc0e9db3b 100644 --- a/api/test/integration/listing.e2e-spec.ts +++ b/api/test/integration/listing.e2e-spec.ts @@ -665,11 +665,7 @@ describe('Listing Controller Tests', () => { describe('duplicate endpoint', () => { it('should duplicate listing, include units', async () => { - const jurisdictionA = await prisma.jurisdictions.create({ - data: jurisdictionFactory(), - }); - await reservedCommunityTypeFactoryAll(jurisdictionA.id, prisma); - const listingData = await listingFactory(jurisdictionA.id, prisma, { + const listingData = await listingFactory(jurisdictionAId, prisma, { numberOfUnits: 2, }); const listing = await prisma.listings.create({ @@ -1002,4 +998,30 @@ describe('Listing Controller Tests', () => { ); }); }); + + describe('mapMarkers endpoint', () => { + it('should find all active listings', async () => { + const listingData = await listingFactory(jurisdictionAId, prisma); + const listing = await prisma.listings.create({ + data: listingData, + }); + + const closedListingData = await listingFactory(jurisdictionAId, prisma, { + status: ListingsStatusEnum.closed, + }); + const closedListing = await prisma.listings.create({ + data: closedListingData, + }); + + const res = await request(app.getHttpServer()) + .get('/listings/mapMarkers') + .expect(200); + + expect(res.body.length).toBeGreaterThanOrEqual(1); + + const ids = res.body.map((marker) => marker.id); + expect(ids).toContain(listing.id); + expect(ids).not.toContain(closedListing.id); + }); + }); }); diff --git a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts index dbe4c8c3b0..63dce5fd22 100644 --- a/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-admin.e2e-spec.ts @@ -1249,6 +1249,14 @@ describe('Testing Permissioning of endpoints as Admin User', () => { expect(activityLogResult).not.toBeNull(); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts index d66ef27e1c..525c627172 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-correct-juris.e2e-spec.ts @@ -1211,6 +1211,14 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the corr expect(activityLogResult).not.toBeNull(); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts index c36c41ff58..332f878f47 100644 --- a/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-juris-admin-wrong-juris.e2e-spec.ts @@ -1149,6 +1149,14 @@ describe('Testing Permissioning of endpoints as Jurisdictional Admin in the wron expect(activityLogResult).not.toBeNull(); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts index 32674de6c4..023267c4b3 100644 --- a/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-no-user.e2e-spec.ts @@ -1061,6 +1061,14 @@ describe('Testing Permissioning of endpoints as logged out user', () => { .set('Cookie', cookies) .expect(403); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts index a31d9eded3..6cd3ac63fa 100644 --- a/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-correct-listing.e2e-spec.ts @@ -1139,6 +1139,14 @@ describe('Testing Permissioning of endpoints as partner with correct listing', ( .set('Cookie', cookies) .expect(200); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts index 7c0bb5876b..694e39f28e 100644 --- a/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-partner-wrong-listing.e2e-spec.ts @@ -1100,6 +1100,14 @@ describe('Testing Permissioning of endpoints as partner with wrong listing', () .set('Cookie', cookies) .expect(200); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts index ec6fb9537b..ee62f6325b 100644 --- a/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts +++ b/api/test/integration/permission-tests/permission-as-public.e2e-spec.ts @@ -1132,6 +1132,14 @@ describe('Testing Permissioning of endpoints as public user', () => { .set('Cookie', cookies) .expect(403); }); + + it('should succeed for mapMarkers endpoint', async () => { + await request(app.getHttpServer()) + .get(`/listings/mapMarkers`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .set('Cookie', cookies) + .expect(200); + }); }); describe('Testing application flagged set endpoints', () => { diff --git a/api/test/unit/services/listing.service.spec.ts b/api/test/unit/services/listing.service.spec.ts index 925e05c62e..a5c97e43a6 100644 --- a/api/test/unit/services/listing.service.spec.ts +++ b/api/test/unit/services/listing.service.spec.ts @@ -3508,4 +3508,27 @@ describe('Testing listing service', () => { expect(prisma.assets.deleteMany).not.toHaveBeenCalled(); }); }); + + describe('Test mapMarkers endpoint', () => { + it('should find all active listings', async () => { + prisma.listings.findMany = jest.fn().mockResolvedValue([ + { + id: 'random id', + listingsBuildingAddress: exampleAddress, + }, + ]); + + await service.mapMarkers(); + + expect(prisma.listings.findMany).toHaveBeenCalledWith({ + select: { + id: true, + listingsBuildingAddress: true, + }, + where: { + status: ListingsStatusEnum.active, + }, + }); + }); + }); }); diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index 1bf782722a..c8ef14963e 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -256,6 +256,20 @@ export class ListingsService { axios(configs, resolve, reject) }) } + /** + * Get listing map markers + */ + mapMarkers(options: IRequestOptions = {}): Promise { + return new Promise((resolve, reject) => { + let url = basePath + "/listings/mapMarkers" + + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + + /** 适配ios13,get请求不允许带body */ + + axios(configs, resolve, reject) + }) + } /** * Get listing for external consumption by id */ @@ -281,48 +295,37 @@ export class ListingsService { }) } /** - * Get listing by id + * Duplicate listing */ - retrieve( + duplicate( params: { - /** */ - id: string - /** */ - view?: ListingViews + /** requestBody */ + body?: ListingDuplicate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/{id}" - url = url.replace("{id}", params["id"] + "") + let url = basePath + "/listings/duplicate" - const configs: IRequestConfig = getConfigs("get", "application/json", url, options) - configs.params = { view: params["view"] } + const configs: IRequestConfig = getConfigs("post", "application/json", url, options) - /** 适配ios13,get请求不允许带body */ + let data = params.body + + configs.data = data axios(configs, resolve, reject) }) } /** - * Update listing by id + * Trigger the listing process job */ - update( - params: { - /** */ - id: string - /** requestBody */ - body?: ListingUpdate - } = {} as any, - options: IRequestOptions = {} - ): Promise { + process(options: IRequestOptions = {}): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/{id}" - url = url.replace("{id}", params["id"] + "") + let url = basePath + "/listings/closeListings" const configs: IRequestConfig = getConfigs("put", "application/json", url, options) - let data = params.body + let data = null configs.data = data @@ -330,19 +333,22 @@ export class ListingsService { }) } /** - * Duplicate listing + * Update listing by id */ - duplicate( + update( params: { + /** */ + id: string /** requestBody */ - body?: ListingDuplicate + body?: ListingUpdate } = {} as any, options: IRequestOptions = {} ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/duplicate" + let url = basePath + "/listings/{id}" + url = url.replace("{id}", params["id"] + "") - const configs: IRequestConfig = getConfigs("post", "application/json", url, options) + const configs: IRequestConfig = getConfigs("put", "application/json", url, options) let data = params.body @@ -352,17 +358,25 @@ export class ListingsService { }) } /** - * Trigger the listing process job + * Get listing by id */ - process(options: IRequestOptions = {}): Promise { + retrieve( + params: { + /** */ + id: string + /** */ + view?: ListingViews + } = {} as any, + options: IRequestOptions = {} + ): Promise { return new Promise((resolve, reject) => { - let url = basePath + "/listings/closeListings" - - const configs: IRequestConfig = getConfigs("put", "application/json", url, options) + let url = basePath + "/listings/{id}" + url = url.replace("{id}", params["id"] + "") - let data = null + const configs: IRequestConfig = getConfigs("get", "application/json", url, options) + configs.params = { view: params["view"] } - configs.data = data + /** 适配ios13,get请求不允许带body */ axios(configs, resolve, reject) }) @@ -3490,6 +3504,17 @@ export interface PaginatedListing { meta: PaginationMeta } +export interface ListingMapMarker { + /** */ + id: string + + /** */ + lat: number + + /** */ + lng: number +} + export interface UnitAmiChartOverrideCreate { /** */ items: AmiChartItem[]