From dc70616a2cf83a5b78c9ee54a9a0a28f2534d10a Mon Sep 17 00:00:00 2001 From: Pedro Perrone Date: Sat, 11 May 2024 21:56:49 -0300 Subject: [PATCH 1/5] Add address details in shelters --- prisma/migrations/20240512005246_/migration.sql | 6 ++++++ prisma/schema.prisma | 5 +++++ src/shelter/shelter.service.ts | 5 +++++ src/shelter/types/types.ts | 5 +++++ 4 files changed, 21 insertions(+) create mode 100644 prisma/migrations/20240512005246_/migration.sql diff --git a/prisma/migrations/20240512005246_/migration.sql b/prisma/migrations/20240512005246_/migration.sql new file mode 100644 index 00000000..c57b86cf --- /dev/null +++ b/prisma/migrations/20240512005246_/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "shelters" ADD COLUMN "city" TEXT, +ADD COLUMN "neighbourhood" TEXT, +ADD COLUMN "street" TEXT, +ADD COLUMN "street_number" TEXT, +ADD COLUMN "zip_code" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 792b1f65..b853d2b4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -89,6 +89,11 @@ model Shelter { name String @unique pix String? @unique address String + street String? + neighbourhood String? + city String? + streetNumber String? @map("street_number") + zipCode String? @map("zip_code") petFriendly Boolean? @map("pet_friendly") shelteredPeople Int? @map("sheltered_people") capacity Int? diff --git a/src/shelter/shelter.service.ts b/src/shelter/shelter.service.ts index a24aa7ab..6b3e984f 100644 --- a/src/shelter/shelter.service.ts +++ b/src/shelter/shelter.service.ts @@ -136,6 +136,11 @@ export class ShelterService { name: true, pix: true, address: true, + street: true, + neighbourhood: true, + city: true, + streetNumber: true, + zipCode: true, capacity: true, contact: true, petFriendly: true, diff --git a/src/shelter/types/types.ts b/src/shelter/types/types.ts index b3911c96..0dc21bac 100644 --- a/src/shelter/types/types.ts +++ b/src/shelter/types/types.ts @@ -12,6 +12,11 @@ const ShelterSchema = z.object({ name: z.string().transform(capitalize), pix: z.string().nullable().optional(), address: z.string().transform(capitalize), + city: z.string().transform(capitalize).nullable().optional(), + neighbourhood: z.string().transform(capitalize).nullable().optional(), + street: z.string().transform(capitalize).nullable().optional(), + streetNumber: z.string().nullable().optional(), + zipCode: z.string().nullable().optional(), petFriendly: z.boolean().nullable().optional(), shelteredPeople: z.number().min(0).nullable().optional(), latitude: z.number().nullable().optional(), From 634411c7cffbd3c09febc6f3e7aa916f1c0ca28b Mon Sep 17 00:00:00 2001 From: AndersonCRocha Date: Sun, 12 May 2024 00:44:17 -0300 Subject: [PATCH 2/5] refactor: remove unused files --- src/prisma/hooks/shelter/index.ts | 0 src/prisma/hooks/shelter/shelter-hooks.ts | 0 src/prisma/hooks/user/index.ts | 0 src/prisma/hooks/user/user-hooks.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/prisma/hooks/shelter/index.ts delete mode 100644 src/prisma/hooks/shelter/shelter-hooks.ts delete mode 100644 src/prisma/hooks/user/index.ts delete mode 100644 src/prisma/hooks/user/user-hooks.ts diff --git a/src/prisma/hooks/shelter/index.ts b/src/prisma/hooks/shelter/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/prisma/hooks/shelter/shelter-hooks.ts b/src/prisma/hooks/shelter/shelter-hooks.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/prisma/hooks/user/index.ts b/src/prisma/hooks/user/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/prisma/hooks/user/user-hooks.ts b/src/prisma/hooks/user/user-hooks.ts deleted file mode 100644 index e69de29b..00000000 From 4bcb785158d01c19cbb8bfa5420f434fbc7ba7a2 Mon Sep 17 00:00:00 2001 From: AndersonCRocha Date: Sun, 12 May 2024 00:44:59 -0300 Subject: [PATCH 3/5] feat: add shelter cities endpoint and shelter cities filter in index endpoint --- src/shelter/ShelterSearch.ts | 21 ++++++++++---- src/shelter/shelter.controller.ts | 11 ++++++++ src/shelter/shelter.service.ts | 35 +++++++++++++++++------ src/shelter/types/search.types.ts | 46 +++++++++++++++++++------------ 4 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/shelter/ShelterSearch.ts b/src/shelter/ShelterSearch.ts index 7598b8e0..ed393d2c 100644 --- a/src/shelter/ShelterSearch.ts +++ b/src/shelter/ShelterSearch.ts @@ -1,10 +1,10 @@ import { Prisma } from '@prisma/client'; -import { PrismaService } from '../prisma/prisma.service'; import { SupplyPriority } from 'src/supply/types'; +import { PrismaService } from '../prisma/prisma.service'; import { - IFilterFormProps, SearchShelterTagResponse, + ShelterSearchProps, ShelterTagInfo, ShelterTagType, } from './types/search.types'; @@ -16,12 +16,12 @@ const defaultTagsData: ShelterTagInfo = { }; class ShelterSearch { - private formProps: Partial; + private formProps: Partial; private prismaService: PrismaService; constructor( prismaService: PrismaService, - props: Partial = {}, + props: Partial = {}, ) { this.prismaService = prismaService; this.formProps = { ...props }; @@ -121,10 +121,21 @@ class ShelterSearch { ]; } + get cities(): Prisma.ShelterWhereInput { + if (!this.formProps.cities) return {}; + + return { + city: { + in: this.formProps.cities, + }, + }; + } + get query(): Prisma.ShelterWhereInput { if (Object.keys(this.formProps).length === 0) return {}; const queryData = { AND: [ + this.cities, { OR: this.search }, { OR: this.shelterStatus }, this.priority(this.formProps.supplyIds), @@ -144,7 +155,7 @@ class ShelterSearch { * @returns Retorna a lista de resultados, adicionando o campo tags em cada supply para assim categoriza-los corretamente e limitar a quantidade de cada retornada respeitando os parametros em formProps */ function parseTagResponse( - tagProps: Partial> = {}, + tagProps: Partial> = {}, results: SearchShelterTagResponse[], voluntaryIds: string[], ): SearchShelterTagResponse[] { diff --git a/src/shelter/shelter.controller.ts b/src/shelter/shelter.controller.ts index 097285e4..b34b96a7 100644 --- a/src/shelter/shelter.controller.ts +++ b/src/shelter/shelter.controller.ts @@ -34,6 +34,17 @@ export class ShelterController { } } + @Get('cities') + async cities() { + try { + const data = await this.shelterService.getCities(); + return new ServerResponse(200, 'Successfully get shelters cities', data); + } catch (err: any) { + this.logger.error(`Failed to get shelters cities: ${err}`); + throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); + } + } + @Get(':id') async show(@Param('id') id: string) { try { diff --git a/src/shelter/shelter.service.ts b/src/shelter/shelter.service.ts index 6b3e984f..61228208 100644 --- a/src/shelter/shelter.service.ts +++ b/src/shelter/shelter.service.ts @@ -1,19 +1,19 @@ -import { z } from 'zod'; import { Injectable } from '@nestjs/common'; -import * as qs from 'qs'; import { Prisma } from '@prisma/client'; import { DefaultArgs } from '@prisma/client/runtime/library'; +import * as qs from 'qs'; +import { z } from 'zod'; import { PrismaService } from '../prisma/prisma.service'; +import { SupplyPriority } from '../supply/types'; +import { SearchSchema } from '../types'; +import { ShelterSearch, parseTagResponse } from './ShelterSearch'; +import { ShelterSearchPropsSchema } from './types/search.types'; import { CreateShelterSchema, FullUpdateShelterSchema, UpdateShelterSchema, } from './types/types'; -import { SearchSchema } from '../types'; -import { ShelterSearch, parseTagResponse } from './ShelterSearch'; -import { SupplyPriority } from '../supply/types'; -import { IFilterFormProps } from './types/search.types'; @Injectable() export class ShelterService { @@ -115,7 +115,7 @@ export class ShelterService { perPage, search: searchQuery, } = SearchSchema.parse(query); - const queryData = qs.parse(searchQuery) as unknown as IFilterFormProps; + const queryData = ShelterSearchPropsSchema.parse(qs.parse(searchQuery)); const { query: where } = new ShelterSearch(this.prismaService, queryData); const count = await this.prismaService.shelter.count({ where }); @@ -177,7 +177,26 @@ export class ShelterService { }; } - loadVoluntaryIds() { + async getCities() { + const cities = await this.prismaService.shelter.groupBy({ + by: ['city'], + _count: { + id: true, + }, + orderBy: { + _count: { + id: 'desc', + }, + }, + }); + + return cities.map(({ city, _count: { id: sheltersCount } }) => ({ + city: city || 'Cidade não informada', + sheltersCount, + })); + } + + private loadVoluntaryIds() { this.prismaService.supplyCategory .findMany({ where: { diff --git a/src/shelter/types/search.types.ts b/src/shelter/types/search.types.ts index b80ca59d..dafd17ff 100644 --- a/src/shelter/types/search.types.ts +++ b/src/shelter/types/search.types.ts @@ -1,26 +1,36 @@ import { Shelter, ShelterSupply, Supply } from '@prisma/client'; +import { z } from 'zod'; import { SupplyPriority } from '../../supply/types'; -export type ShelterAvailabilityStatus = 'available' | 'unavailable' | 'waiting'; +const ShelterTagTypeSchema = z.enum([ + 'NeedVolunteers', + 'NeedDonations', + 'RemainingSupplies', +]); -export interface IFilterFormProps { - search: string; - priority: SupplyPriority | null; - supplyCategoryIds: string[]; - supplyIds: string[]; - shelterStatus: ShelterAvailabilityStatus[]; - tags: ShelterTagInfo | null; -} +const ShelterTagInfoSchema = z.record( + ShelterTagTypeSchema, + z.number().optional(), +); -export type SearchShelterTagResponse = Shelter & { - shelterSupplies: (ShelterSupply & { supply: Supply })[]; -}; +export type ShelterTagType = z.infer; + +export type ShelterTagInfo = z.infer; -export type ShelterTagType = - | 'NeedVolunteers' - | 'NeedDonations' - | 'RemainingSupplies'; +export const ShelterSearchPropsSchema = z.object({ + search: z.string().optional(), + priority: z.preprocess(Number, z.nativeEnum(SupplyPriority).optional()), + supplyCategoryIds: z.array(z.string()).optional(), + supplyIds: z.array(z.string()).optional(), + shelterStatus: z + .array(z.enum(['available', 'unavailable', 'waiting'])) + .optional(), + tags: ShelterTagInfoSchema.nullable().optional(), + cities: z.array(z.string()).optional(), +}); -export type ShelterTagInfo = { - [key in ShelterTagType]?: number; +export type ShelterSearchProps = z.infer; + +export type SearchShelterTagResponse = Shelter & { + shelterSupplies: (ShelterSupply & { supply: Supply })[]; }; From 76bff1654051f134346312acddaca89adea1beeb Mon Sep 17 00:00:00 2001 From: AndersonCRocha Date: Sun, 12 May 2024 00:48:01 -0300 Subject: [PATCH 4/5] feat: add address fields in 'show' endpoint return --- src/shelter/shelter.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shelter/shelter.service.ts b/src/shelter/shelter.service.ts index 61228208..b3b7f7fe 100644 --- a/src/shelter/shelter.service.ts +++ b/src/shelter/shelter.service.ts @@ -69,6 +69,11 @@ export class ShelterService { id: true, name: true, address: true, + street: true, + neighbourhood: true, + city: true, + streetNumber: true, + zipCode: true, pix: true, shelteredPeople: true, capacity: true, From 01a274b34402810c015d54dd999096e953fed33c Mon Sep 17 00:00:00 2001 From: AndersonCRocha Date: Sun, 12 May 2024 01:30:35 -0300 Subject: [PATCH 5/5] refactor: improvements in search logic --- src/shelter/ShelterSearch.ts | 92 +++++++++++++++---------------- src/shelter/types/search.types.ts | 13 +++-- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/shelter/ShelterSearch.ts b/src/shelter/ShelterSearch.ts index ed393d2c..205f97e8 100644 --- a/src/shelter/ShelterSearch.ts +++ b/src/shelter/ShelterSearch.ts @@ -5,6 +5,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { SearchShelterTagResponse, ShelterSearchProps, + ShelterStatus, ShelterTagInfo, ShelterTagType, } from './types/search.types'; @@ -28,45 +29,42 @@ class ShelterSearch { } priority(supplyIds: string[] = []): Prisma.ShelterWhereInput { - if (this.formProps.priority) { - return { - shelterSupplies: { - some: { - priority: +this.formProps.priority, - supplyId: - supplyIds.length > 0 - ? { - in: supplyIds, - } - : undefined, - }, + if (!this.formProps.priority) return {}; + + return { + shelterSupplies: { + some: { + priority: +this.formProps.priority, + supplyId: + supplyIds.length > 0 + ? { + in: supplyIds, + } + : undefined, }, - }; - } else return {}; + }, + }; } get shelterStatus(): Prisma.ShelterWhereInput[] { if (!this.formProps.shelterStatus) return []; - else { - return this.formProps.shelterStatus.map((status) => { - if (status === 'waiting') - return { - capacity: null, - }; - else if (status === 'available') - return { - capacity: { - gt: this.prismaService.shelter.fields.shelteredPeople, - }, - }; - else - return { - capacity: { - lte: this.prismaService.shelter.fields.shelteredPeople, - }, - }; - }); - } + + const clausesFromStatus: Record< + ShelterStatus, + Prisma.ShelterWhereInput['capacity'] | null + > = { + waiting: null, + available: { + gt: this.prismaService.shelter.fields.shelteredPeople, + }, + unavailable: { + lte: this.prismaService.shelter.fields.shelteredPeople, + }, + }; + + return this.formProps.shelterStatus.map((status) => ({ + capacity: clausesFromStatus[status], + })); } supplyCategoryIds( @@ -104,21 +102,21 @@ class ShelterSearch { get search(): Prisma.ShelterWhereInput[] { if (!this.formProps.search) return []; - else - return [ - { - address: { - contains: this.formProps.search, - mode: 'insensitive', - }, + + return [ + { + address: { + contains: this.formProps.search, + mode: 'insensitive', }, - { - name: { - contains: this.formProps.search, - mode: 'insensitive', - }, + }, + { + name: { + contains: this.formProps.search, + mode: 'insensitive', }, - ]; + }, + ]; } get cities(): Prisma.ShelterWhereInput { diff --git a/src/shelter/types/search.types.ts b/src/shelter/types/search.types.ts index dafd17ff..8566df7d 100644 --- a/src/shelter/types/search.types.ts +++ b/src/shelter/types/search.types.ts @@ -2,6 +2,10 @@ import { Shelter, ShelterSupply, Supply } from '@prisma/client'; import { z } from 'zod'; import { SupplyPriority } from '../../supply/types'; +const ShelterStatusSchema = z.enum(['available', 'unavailable', 'waiting']); + +export type ShelterStatus = z.infer; + const ShelterTagTypeSchema = z.enum([ 'NeedVolunteers', 'NeedDonations', @@ -19,12 +23,13 @@ export type ShelterTagInfo = z.infer; export const ShelterSearchPropsSchema = z.object({ search: z.string().optional(), - priority: z.preprocess(Number, z.nativeEnum(SupplyPriority).optional()), + priority: z.preprocess( + (value) => Number(value) || undefined, + z.nativeEnum(SupplyPriority).optional(), + ), supplyCategoryIds: z.array(z.string()).optional(), supplyIds: z.array(z.string()).optional(), - shelterStatus: z - .array(z.enum(['available', 'unavailable', 'waiting'])) - .optional(), + shelterStatus: z.array(ShelterStatusSchema).optional(), tags: ShelterTagInfoSchema.nullable().optional(), cities: z.array(z.string()).optional(), });