From 2954d7a9a8e052afeeab3598305a706b404499c5 Mon Sep 17 00:00:00 2001 From: Filipe Pacheco de Fraga Date: Tue, 14 May 2024 00:30:07 -0300 Subject: [PATCH] Release 0.0.1 (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jhonatan David <82552187+jotahdavid@users.noreply.github.com> Co-authored-by: MatheusDubin Co-authored-by: Gustavo Clemente Co-authored-by: kelvinsb Co-authored-by: Tatiane Co-authored-by: Sombrio Co-authored-by: AlbuquerqueRafael Co-authored-by: José Fagundes Co-authored-by: Arthur <40851238+4rthuurr@users.noreply.github.com> Co-authored-by: Giovanni Bassi <334958+giggio@users.noreply.github.com> Co-authored-by: LeoFC97 Co-authored-by: Rogério Piatek <74259272+RogerioPiatek@users.noreply.github.com> Co-authored-by: Anderson Rocha <59173445+AndersonCRocha@users.noreply.github.com> Co-authored-by: Pedro Perrone --- .github/workflows/ci.yml | 32 +++++ .github/workflows/pull_request_template.md | 24 ++++ Dockerfile | 4 +- README.md | 13 +- docker-compose.dev.yml | 23 +-- package.json | 1 + .../migrations/20240512005246_/migration.sql | 6 + prisma/schema.prisma | 5 + .../UserDecorator/user.decorator.ts | 8 ++ src/guards/apply-user.guard.ts | 10 ++ src/guards/utils.ts | 38 +++-- src/shelter/ShelterSearch.ts | 134 +++++++++++------- src/shelter/shelter.controller.ts | 20 ++- src/shelter/shelter.service.ts | 50 +++++-- src/shelter/types/search.types.ts | 62 +++++--- src/shelter/types/types.ts | 5 + src/utils/utils.ts | 42 +++++- tsconfig.json | 3 + 18 files changed, 366 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pull_request_template.md create mode 100644 prisma/migrations/20240512005246_/migration.sql create mode 100644 src/decorators/UserDecorator/user.decorator.ts create mode 100644 src/guards/apply-user.guard.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f03015e0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + branches: + - master + - develop + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Check out the source + - name: Checkout Source + uses: actions/checkout@v4 + # Setup node.js and cache + - name: "Setup node.js" + uses: actions/setup-node@v4 + with: + node-version: "18.x" + cache: 'npm' + cache-dependency-path: ./package-lock.json + # Install dependencies + - name: Install dependencies + run: npm ci + # Lint App + - name: Lint App + run: npm run lint:ci + # Build App + - name: Build App + run: npm run build \ No newline at end of file diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 00000000..d9d9ffd2 --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,24 @@ +### 🔨 Tenho uma nova PR para vocês revisarem + +- 🤔 O que foi feito? +> Digite aqui... + + +### 📗 Checklist do desenvolvedor + +- [ ] Foi testado localmente? +- [ ] Foi adicionado documentação necessária (swagger, testes e etc)? + +### 👀 Checklist do revisor + +#### Revisor 1️⃣ + +- [ ] Você entendeu o propósito desse PR? +- [ ] Você entendeu o fluxo de negócio? +- [ ] Você entendeu o que e como foi desenvolvido tecnicamente a solução? +- [ ] Você analisou se os testes estão cobrindo a maioria dos casos? + + +### 🔗 Referênia + +[Issue XX](https://github.com/SOS-RS/backend/issues/XX) diff --git a/Dockerfile b/Dockerfile index 4c2dba54..a98af1c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM node:18.18 as node +FROM node:18.18-alpine as node WORKDIR /usr/app COPY package.json package-lock.json ./ RUN npm install -COPY . . \ No newline at end of file +COPY . . diff --git a/README.md b/README.md index 6a231353..99e4c850 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # 🌊 Backend para App de Ajuda em Enchentes 🌊 -Este repositório contém o backend de um aplicativo projetado para ajudar na organização e distribuição de suprimentos, bem como na coordenação de voluntários durante enchentes no Rio Grande do Sul. Ele fornece APIs essenciais para a autenticação de usuários, gerenciamento de abrigos e suprimentos, e muito mais. +Este repositório contém o backend de um aplicativo projetado para ajudar na organização e distribuição de suprimentos, +bem como na coordenação de voluntários durante enchentes no Rio Grande do Sul. Ele fornece APIs essenciais para a +autenticação de usuários, gerenciamento de abrigos e suprimentos, e muito mais. + +Se você quiser discutir ideias, problemas ou contribuições, sinta-se à vontade para se juntar ao nosso servidor do +Discord [aqui](https://discord.gg/vjZS6BQXvM). ## 🛠 Tecnologias Utilizadas @@ -12,7 +17,8 @@ Este repositório contém o backend de um aplicativo projetado para ajudar na or ## 🗂 Dump do Banco de Dados -Para iniciar com dados de exemplo, utilize o dump do banco disponível em `prisma/migration/dev_dump.sql`. Este arquivo pode ser executado após as migrations estarem aplicadas. +Para iniciar com dados de exemplo, utilize o dump do banco disponível em `prisma/migration/dev_dump.sql`. Este arquivo +pode ser executado após as migrations estarem aplicadas. Se estiver usando Docker, os comandos para carregar o dump são: @@ -96,6 +102,7 @@ ports: ## 🤝 Contribuição -Contribuições são muito bem-vindas! Se deseja ajudar, faça um fork do repositório, crie uma branch com suas modificações, e envie um pull request. +Contribuições são muito bem-vindas! Se deseja ajudar, faça um fork do repositório, crie uma branch com suas +modificações, e envie um pull request. Sua ajuda é crucial para apoiar a comunidade afetada pelas enchentes no Rio Grande do Sul! diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3b49bcff..ba40eebc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -3,31 +3,34 @@ version: '3.8' services: api: container_name: sos-rs-api - image: node:18.18 + image: node:18.18-alpine restart: always tty: true depends_on: - db ports: - - '4000:4000' + - '${PORT}:${PORT}' volumes: - .:/usr/app - /usr/app/node_modules working_dir: '/usr/app' environment: - - DB_HOST=sos-rs-db - - DB_PORT=5432 - - DB_DATABASE_NAME=sos_rs - - DB_USER=root - - DB_PASSWORD=root + - DB_HOST=${DB_HOST} + - DB_PORT=${DB_PORT} + - DB_DATABASE_NAME=${DB_DATABASE_NAME} + - DB_USER=${DB_USER} + - DB_PASSWORD=${DB_PASSWORD} + - PORT=${PORT} command: > sh -c "npm install && npx prisma generate && npx prisma migrate dev && - npm run start:dev" + npm run start:dev -- --preserveWatchOutput" db: container_name: sos-rs-db image: postgres + ports: + - '${DB_PORT}:${DB_PORT}' environment: - - POSTGRES_PASSWORD=root - - POSTGRES_USER=root + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_USER=${DB_USER} diff --git a/package.json b/package.json index 5e154b46..3212f2fd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint:ci": "eslint \"{src,apps,libs,test}/**/*.ts\" --format=stylish", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", 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/decorators/UserDecorator/user.decorator.ts b/src/decorators/UserDecorator/user.decorator.ts new file mode 100644 index 00000000..67e5edcd --- /dev/null +++ b/src/decorators/UserDecorator/user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const UserDecorator = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request?.user; + }, +); diff --git a/src/guards/apply-user.guard.ts b/src/guards/apply-user.guard.ts new file mode 100644 index 00000000..07e7a6de --- /dev/null +++ b/src/guards/apply-user.guard.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class ApplyUser extends AuthGuard('jwt') { + handleRequest(err: any, user: any) { + if (user) return user; + return null; + } +} diff --git a/src/guards/utils.ts b/src/guards/utils.ts index 1e385a4c..f36066e3 100644 --- a/src/guards/utils.ts +++ b/src/guards/utils.ts @@ -11,22 +11,34 @@ async function canActivate(context: ExecutionContext, allowed: AccessLevel[]) { if (request.user) { const { userId, sessionId } = request.user; - const session = await service.session.findUnique({ - where: { id: sessionId, active: true, user: { id: userId } }, - include: { - user: true, - }, - }); - - if ( - session && - allowed.some((permission) => permission === session.user.accessLevel) - ) { - return true; - } + return isRightSessionRole(allowed, sessionId, userId); } return false; } +async function isRightSessionRole( + allowed: AccessLevel[], + sessionId?: string, + userId?: string, +) { + if (!sessionId) return false; + if (!userId) return false; + + const session = await service.session.findUnique({ + where: { id: sessionId, active: true, user: { id: userId } }, + include: { + user: true, + }, + }); + + if ( + session && + allowed.some((permission) => permission === session.user.accessLevel) + ) { + return true; + } + return false; +} + export { canActivate }; diff --git a/src/shelter/ShelterSearch.ts b/src/shelter/ShelterSearch.ts index 7598b8e0..5215d37c 100644 --- a/src/shelter/ShelterSearch.ts +++ b/src/shelter/ShelterSearch.ts @@ -1,10 +1,12 @@ import { Prisma } from '@prisma/client'; -import { PrismaService } from '../prisma/prisma.service'; +import { calculateGeolocationBounds } from '@/utils/utils'; import { SupplyPriority } from 'src/supply/types'; +import { PrismaService } from '../prisma/prisma.service'; import { - IFilterFormProps, SearchShelterTagResponse, + ShelterSearchProps, + ShelterStatus, ShelterTagInfo, ShelterTagType, } from './types/search.types'; @@ -16,57 +18,54 @@ 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 }; } 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,27 +103,58 @@ 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 { + if (!this.formProps.cities) return {}; + + return { + city: { + in: this.formProps.cities, + }, + }; + } + + get geolocation(): Prisma.ShelterWhereInput { + if (!this.formProps.geolocation) return {}; + + const { minLat, maxLat, minLong, maxLong } = calculateGeolocationBounds( + this.formProps.geolocation, + ); + + return { + latitude: { + gte: minLat, + lte: maxLat, + }, + longitude: { + gte: minLong, + lte: maxLong, + }, + }; } get query(): Prisma.ShelterWhereInput { if (Object.keys(this.formProps).length === 0) return {}; const queryData = { AND: [ + this.cities, + this.geolocation, { OR: this.search }, { OR: this.shelterStatus }, this.priority(this.formProps.supplyIds), @@ -144,7 +174,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..24603857 100644 --- a/src/shelter/shelter.controller.ts +++ b/src/shelter/shelter.controller.ts @@ -15,6 +15,8 @@ import { ApiTags } from '@nestjs/swagger'; import { ShelterService } from './shelter.service'; import { ServerResponse } from '../utils'; import { StaffGuard } from '@/guards/staff.guard'; +import { ApplyUser } from '@/guards/apply-user.guard'; +import { UserDecorator } from '@/decorators/UserDecorator/user.decorator'; @ApiTags('Abrigos') @Controller('shelters') @@ -34,10 +36,24 @@ 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) { + @UseGuards(ApplyUser) + async show(@UserDecorator() user: any, @Param('id') id: string) { try { - const data = await this.shelterService.show(id); + const isLogged = + Boolean(user) && Boolean(user?.sessionId) && Boolean(user?.userId); + const data = await this.shelterService.show(id, isLogged); return new ServerResponse(200, 'Successfully get shelter', data); } catch (err: any) { this.logger.error(`Failed to get shelter: ${err}`); diff --git a/src/shelter/shelter.service.ts b/src/shelter/shelter.service.ts index a24aa7ab..e3883215 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 { @@ -60,7 +60,7 @@ export class ShelterService { }); } - async show(id: string) { + async show(id: string, shouldShowContact: boolean) { const data = await this.prismaService.shelter.findFirst({ where: { id, @@ -69,10 +69,15 @@ 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, - contact: true, + contact: shouldShowContact, petFriendly: true, prioritySum: true, latitude: true, @@ -115,7 +120,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 }); @@ -136,8 +141,12 @@ 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, shelteredPeople: true, prioritySum: true, @@ -172,7 +181,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..b87e653a 100644 --- a/src/shelter/types/search.types.ts +++ b/src/shelter/types/search.types.ts @@ -1,26 +1,52 @@ import { Shelter, ShelterSupply, Supply } from '@prisma/client'; +import { z } from 'zod'; import { SupplyPriority } from '../../supply/types'; -export type ShelterAvailabilityStatus = 'available' | 'unavailable' | 'waiting'; +const ShelterStatusSchema = z.enum(['available', 'unavailable', 'waiting']); -export interface IFilterFormProps { - search: string; - priority: SupplyPriority | null; - supplyCategoryIds: string[]; - supplyIds: string[]; - shelterStatus: ShelterAvailabilityStatus[]; - tags: ShelterTagInfo | null; -} +export type ShelterStatus = z.infer; -export type SearchShelterTagResponse = Shelter & { - shelterSupplies: (ShelterSupply & { supply: Supply })[]; -}; +const ShelterTagTypeSchema = z.enum([ + 'NeedVolunteers', + 'NeedDonations', + 'RemainingSupplies', +]); + +const ShelterTagInfoSchema = z.record( + ShelterTagTypeSchema, + z.number().optional(), +); + +export type ShelterTagType = z.infer; + +export type ShelterTagInfo = z.infer; -export type ShelterTagType = - | 'NeedVolunteers' - | 'NeedDonations' - | 'RemainingSupplies'; +export const GeolocationFilterSchema = z.object({ + latitude: z.coerce.number(), + longitude: z.coerce.number(), + radiusInMeters: z.coerce.number(), +}); -export type ShelterTagInfo = { - [key in ShelterTagType]?: number; +export type GeolocationFilter = z.infer; + +export const ShelterSearchPropsSchema = z.object({ + search: z.string().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(ShelterStatusSchema).optional(), + tags: ShelterTagInfoSchema.nullable().optional(), + cities: z.array(z.string()).optional(), + geolocation: GeolocationFilterSchema.optional(), +}); + +export type ShelterSearchProps = z.infer; + +type AllowedShelterFields = Omit; + +export type SearchShelterTagResponse = AllowedShelterFields & { + shelterSupplies: (ShelterSupply & { supply: Supply })[]; }; 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(), diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 772b3e12..dfcbc70f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,5 @@ import { Logger } from '@nestjs/common'; +import { GeolocationFilter } from 'src/shelter/types/search.types'; class ServerResponse { readonly message: string; @@ -75,10 +76,45 @@ function deepMerge(target: Record, source: Record) { } } +interface Coordinates { + maxLat: number; + minLat: number; + maxLong: number; + minLong: number; +} + +function calculateGeolocationBounds({ + latitude, + longitude, + radiusInMeters, +}: GeolocationFilter): Coordinates { + const earthRadius = 6371000; + + const latRad = (latitude * Math.PI) / 180; + + const radiusRad = radiusInMeters / earthRadius; + + const maxLat = latitude + radiusRad * (180 / Math.PI); + const minLat = latitude - radiusRad * (180 / Math.PI); + + const deltaLong = Math.asin(Math.sin(radiusRad) / Math.cos(latRad)); + + const maxLong = longitude + deltaLong * (180 / Math.PI); + const minLong = longitude - deltaLong * (180 / Math.PI); + + return { + maxLat, + minLat, + maxLong, + minLong, + }; +} + export { ServerResponse, - removeNotNumbers, - getSessionData, - deepMerge, + calculateGeolocationBounds, capitalize, + deepMerge, + getSessionData, + removeNotNumbers, }; diff --git a/tsconfig.json b/tsconfig.json index b5b552bd..acda4d9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,8 @@ "@/guards/*": ["./src/guards/*"], "@/guards": ["./src/guards"] } + }, + "watchOptions": { + "watchFile": "fixedPollingInterval" } }