├── .env.example ├── .env.local ├── .env.test ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ ├── deploy.yml │ ├── dev.yml │ ├── pull_request_template.md │ └── staging.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.dev.yml ├── docker-compose.yml ├── docs ├── endpoints.md └── sos-rs.insomnia.json ├── jest.config.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── prisma ├── dev_dump.sql ├── migrations │ ├── 20240505182421_ │ │ └── migration.sql │ ├── 20240505185353_ │ │ └── migration.sql │ ├── 20240505185534_ │ │ └── migration.sql │ ├── 20240505190155_ │ │ └── migration.sql │ ├── 20240505191728_ │ │ └── migration.sql │ ├── 20240505201711_ │ │ └── migration.sql │ ├── 20240505203201_ │ │ └── migration.sql │ ├── 20240505222048_ │ │ └── migration.sql │ ├── 20240505222318_ │ │ └── migration.sql │ ├── 20240505224712_ │ │ └── migration.sql │ ├── 20240505225104_20240505222318 │ │ └── migration.sql │ ├── 20240506015214_ │ │ └── migration.sql │ ├── 20240506021537_ │ │ └── migration.sql │ ├── 20240506195247_ │ │ └── migration.sql │ ├── 20240507205058_ │ │ └── migration.sql │ ├── 20240507221950_ │ │ └── migration.sql │ ├── 20240508022250_ │ │ └── migration.sql │ ├── 20240508041443_ │ │ └── migration.sql │ ├── 20240508050213_ │ │ └── migration.sql │ ├── 20240508150340_ │ │ └── migration.sql │ ├── 20240508193500_ │ │ └── migration.sql │ ├── 20240509235041_ │ │ └── migration.sql │ ├── 20240510225124_ │ │ └── migration.sql │ ├── 20240511054108_ │ │ └── migration.sql │ ├── 20240512005246_ │ │ └── migration.sql │ ├── 20240514211840_ │ │ └── migration.sql │ ├── 20240515003750_ │ │ └── migration.sql │ ├── 20240516140110_add_capacity_and_shelter_pets │ │ └── migration.sql │ ├── 20240516212629_ │ │ └── migration.sql │ ├── 20240517181431_add_unaccent_extension │ │ └── migration.sql │ ├── 20240517192040_ │ │ └── migration.sql │ ├── 20240522191544_ │ │ └── migration.sql │ ├── 20240522195717_ │ │ └── migration.sql │ ├── 20240522200429_ │ │ └── migration.sql │ ├── 20240522220544_ │ │ └── migration.sql │ ├── 20240523211341_ │ │ └── migration.sql │ ├── 20240528182902_ │ │ └── migration.sql │ ├── 20240605140756_ │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── src ├── app.module.ts ├── dashboard │ ├── dashboard.controller.spec.ts │ ├── dashboard.controller.ts │ ├── dashboard.module.ts │ ├── dashboard.service.spec.ts │ └── dashboard.service.ts ├── decorators │ ├── Hmac │ │ ├── hmac.decorator.ts │ │ └── index.ts │ ├── RegisterShelterSupplyHistory │ │ ├── RegisterShelterSupplyHistory.decorator.ts │ │ └── index.ts │ └── UserDecorator │ │ └── user.decorator.ts ├── donation-order │ ├── donation-order.controller.spec.ts │ ├── donation-order.controller.ts │ ├── donation-order.module.ts │ ├── donation-order.service.spec.ts │ ├── donation-order.service.ts │ └── types.ts ├── guards │ ├── admin.guard.ts │ ├── apply-user.guard.ts │ ├── distribution-center.guard.ts │ ├── hmac.guard.ts │ ├── jwt-auth.guard.ts │ ├── staff.guard.ts │ ├── user.guard.ts │ └── utils.ts ├── interceptors │ ├── index.ts │ └── interceptors │ │ ├── index.ts │ │ ├── server-response │ │ ├── index.ts │ │ └── server-response.interceptor.ts │ │ └── shelter-supply-history │ │ ├── index.ts │ │ ├── shelter-supply-history.interceptor.ts │ │ ├── types.ts │ │ └── utils.ts ├── main.ts ├── middlewares │ └── logging.middleware.ts ├── partners │ ├── partners.controller.spec.ts │ ├── partners.controller.ts │ ├── partners.module.ts │ ├── partners.service.spec.ts │ ├── partners.service.ts │ └── types.ts ├── prisma │ ├── hooks │ │ └── user │ │ │ ├── index.ts │ │ │ └── user-hooks.ts │ ├── prisma.module.ts │ ├── prisma.service.spec.ts │ └── prisma.service.ts ├── sessions │ ├── jwt.strategy.ts │ ├── sessions.controller.spec.ts │ ├── sessions.controller.ts │ ├── sessions.module.ts │ ├── sessions.service.spec.ts │ ├── sessions.service.ts │ └── types.ts ├── shelter-managers │ ├── shelter-managers.controller.spec.ts │ ├── shelter-managers.controller.ts │ ├── shelter-managers.module.ts │ ├── shelter-managers.service.spec.ts │ ├── shelter-managers.service.ts │ └── types.ts ├── shelter-supply │ ├── shelter-supply.controller.spec.ts │ ├── shelter-supply.controller.ts │ ├── shelter-supply.module.ts │ ├── shelter-supply.service.spec.ts │ ├── shelter-supply.service.ts │ └── types.ts ├── shelter │ ├── ShelterSearch.ts │ ├── shelter.controller.spec.ts │ ├── shelter.controller.ts │ ├── shelter.module.ts │ ├── shelter.service.spec.ts │ ├── shelter.service.ts │ └── types │ │ ├── search.types.ts │ │ └── types.ts ├── supplies-history │ ├── supplies-history.controller.spec.ts │ ├── supplies-history.controller.ts │ ├── supplies-history.module.ts │ ├── supplies-history.service.spec.ts │ ├── supplies-history.service.ts │ └── types.ts ├── supply-categories │ ├── supply-categories.controller.spec.ts │ ├── supply-categories.controller.ts │ ├── supply-categories.module.ts │ ├── supply-categories.service.spec.ts │ ├── supply-categories.service.ts │ └── types.ts ├── supply │ ├── supply.controller.spec.ts │ ├── supply.controller.ts │ ├── supply.module.ts │ ├── supply.service.spec.ts │ ├── supply.service.ts │ └── types.ts ├── supporters │ ├── supporters.controller.spec.ts │ ├── supporters.controller.ts │ ├── supporters.module.ts │ ├── supporters.service.spec.ts │ ├── supporters.service.ts │ └── types.ts ├── types.ts ├── users │ ├── types.ts │ ├── users.controller.spec.ts │ ├── users.controller.ts │ ├── users.module.ts │ ├── users.service.spec.ts │ └── users.service.ts └── utils │ ├── index.ts │ └── utils.ts ├── test ├── app.e2e-spec.ts └── jest.e2e.config.ts ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | TZ=America/Sao_Paulo 2 | 3 | DB_HOST= 4 | DB_PORT= 5 | DB_USER= 6 | DB_PASSWORD= 7 | DB_DATABASE_NAME= 8 | DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE_NAME}?schema=public" 9 | SECRET_KEY= 10 | HMAC_SECRET_KEY= 11 | 12 | HOST=::0.0.0.0 13 | PORT=4000 -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | TZ=America/Sao_Paulo 2 | 3 | DB_HOST=sos-rs-db 4 | DB_PORT=5432 5 | DB_DATABASE_NAME=sos_rs 6 | DB_USER=root 7 | DB_PASSWORD=root 8 | DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE_NAME}?schema=public" 9 | 10 | SECRET_KEY=batata 11 | HMAC_SECRET_KEY= 12 | 13 | HOST=::0.0.0.0 14 | PORT=4000 -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DB_HOST=localhost 2 | DB_DATABASE_NAME=sos_rs_test 3 | DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE_NAME}?schema=public" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin', 'jest'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | 'plugin:jest/recommended', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | ignorePatterns: ['.eslintrc.js'], 20 | rules: { 21 | '@typescript-eslint/interface-name-prefix': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | 'jest/expect-expect': [ 26 | 'warn', 27 | { 28 | assertFunctionNames: ['expect', 'request.**.expect'], 29 | }, 30 | ], 31 | 'prettier/prettier': [ 32 | 'error', 33 | { 34 | endOfLine: 'auto', 35 | }, 36 | ], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - '*' 10 | pull_request: 11 | branches: 12 | - main 13 | - develop 14 | - staging 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | services: 20 | db: 21 | image: postgres:16.3 22 | ports: 23 | - 5432:5432 24 | env: 25 | POSTGRES_PASSWORD: root 26 | POSTGRES_USER: root 27 | options: >- 28 | --health-cmd pg_isready 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | 33 | steps: 34 | # Check out the source 35 | - name: Checkout Source 36 | uses: actions/checkout@v4 37 | # Setup node.js and cache 38 | - name: 'Setup node.js' 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: '18.x' 42 | cache: 'npm' 43 | cache-dependency-path: ./package-lock.json 44 | # Install dependencies 45 | - name: Install dependencies 46 | run: npm ci 47 | # Lint App 48 | - name: Lint App 49 | run: npm run lint:ci 50 | # Build App 51 | - name: Build App 52 | run: npm run build 53 | - name: Test 54 | run: npm test 55 | - name: Test e2e 56 | run: npm run test:e2e 57 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Backend 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | if: github.repository == 'SOS-RS/backend' 10 | runs-on: self-hosted 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Create .env file 16 | run: | 17 | touch .env 18 | echo TZ=${{ secrets.TZ }} >> .env 19 | echo DB_HOST=${{ secrets.DB_HOST }} >> .env 20 | echo DB_PORT=${{ secrets.DB_PORT }} >> .env 21 | echo DB_USER=${{ secrets.DB_USER }} >> .env 22 | echo DB_PASSWORD=${{ secrets.DB_PASSWORD }} >> .env 23 | echo DB_DATABASE_NAME=${{ secrets.DB_DATABASE_NAME }} >> .env 24 | echo DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/${{ secrets.DB_DATABASE_NAME }}?schema=public >> .env 25 | echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env 26 | echo HOST=${{ secrets.HOST }} >> .env 27 | echo PORT=${{ secrets.PORT }} >> .env 28 | echo HMAC_SECRET_KEY=${{ secrets.HMAC_SECRET_KEY }} >> .env 29 | echo SERVER_USER_PASSWORD=${{ secrets.SERVER_USER_PASSWORD }} >> .env 30 | cat .env 31 | 32 | - name: Remove old docker image 33 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose down --rmi all 34 | 35 | - name: Create new docker image 36 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose up -d --force-recreate 37 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Backend in Development Enviroment 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | 7 | jobs: 8 | build: 9 | if: github.repository == 'SOS-RS/backend' 10 | runs-on: dev 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Create .env file 16 | run: | 17 | touch .env 18 | echo TZ=${{ secrets.TZ }} >> .env 19 | echo DB_HOST=${{ secrets.DB_HOST }} >> .env 20 | echo DB_PORT=${{ secrets.DB_PORT }} >> .env 21 | echo DB_USER=${{ secrets.DB_USER }} >> .env 22 | echo DB_PASSWORD=${{ secrets.DB_PASSWORD }} >> .env 23 | echo DB_DATABASE_NAME=${{ secrets.DEV_DB_DATABASE_NAME }} >> .env 24 | echo DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/${{ secrets.DEV_DB_DATABASE_NAME }}?schema=public >> .env 25 | echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env 26 | echo HOST=${{ secrets.HOST }} >> .env 27 | echo PORT=${{ secrets.PORT }} >> .env 28 | echo HMAC_SECRET_KEY=${{ secrets.HMAC_SECRET_KEY }} >> .env 29 | echo SERVER_USER_PASSWORD=${{ secrets.SERVER_USER_PASSWORD }} >> .env 30 | cat .env 31 | 32 | - name: Remove old docker image 33 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose down --rmi all 34 | 35 | - name: Create new docker image 36 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose up -d --force-recreate 37 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 🔨 Tenho uma nova PR para vocês revisarem 2 | 3 | - 🤔 O que foi feito? 4 | > Digite aqui... 5 | 6 | 7 | ### 📗 Checklist do desenvolvedor 8 | 9 | - [ ] Foi testado localmente? 10 | - [ ] Foi adicionado documentação necessária (swagger, testes e etc)? 11 | 12 | ### 👀 Checklist do revisor 13 | 14 | #### Revisor 1️⃣ 15 | 16 | - [ ] Você entendeu o propósito desse PR? 17 | - [ ] Você entendeu o fluxo de negócio? 18 | - [ ] Você entendeu o que e como foi desenvolvido tecnicamente a solução? 19 | - [ ] Você analisou se os testes estão cobrindo a maioria dos casos? 20 | 21 | 22 | ### 🔗 Referênia 23 | 24 | [Issue XX](https://github.com/SOS-RS/backend/issues/XX) 25 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Backend in Development Enviroment 2 | 3 | on: 4 | push: 5 | branches: [staging] 6 | 7 | jobs: 8 | build: 9 | if: github.repository == 'SOS-RS/backend' 10 | runs-on: stg 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Create .env file 16 | run: | 17 | touch .env 18 | echo TZ=${{ secrets.TZ }} >> .env 19 | echo DB_HOST=${{ secrets.DB_HOST }} >> .env 20 | echo DB_PORT=${{ secrets.DB_PORT }} >> .env 21 | echo DB_USER=${{ secrets.DB_USER }} >> .env 22 | echo DB_PASSWORD=${{ secrets.DB_PASSWORD }} >> .env 23 | echo DB_DATABASE_NAME=${{ secrets.STG_DB_DATABASE_NAME }} >> .env 24 | echo DATABASE_URL=postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOST }}:${{ secrets.DB_PORT }}/${{ secrets.STG_DB_DATABASE_NAME }}?schema=public >> .env 25 | echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env 26 | echo HOST=${{ secrets.HOST }} >> .env 27 | echo PORT=${{ secrets.PORT }} >> .env 28 | echo HMAC_SECRET_KEY=${{ secrets.HMAC_SECRET_KEY }} >> .env 29 | echo SERVER_USER_PASSWORD=${{ secrets.SERVER_USER_PASSWORD }} >> .env 30 | cat .env 31 | 32 | - name: Remove old docker image 33 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose down --rmi all 34 | 35 | - name: Create new docker image 36 | run: echo ${{ secrets.SERVER_USER_PASSWORD }} | sudo -S docker compose up -d --force-recreate 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | .env.development* 7 | .env.production* 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | pnpm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # OS 19 | .DS_Store 20 | 21 | # Tests 22 | /coverage 23 | /.nyc_output 24 | 25 | # IDEs and editors 26 | /.idea 27 | .project 28 | .classpath 29 | .c9/ 30 | *.launch 31 | .settings/ 32 | *.sublime-workspace 33 | 34 | # IDE - VSCode 35 | .vscode/* 36 | !.vscode/settings.json 37 | !.vscode/tasks.json 38 | !.vscode/launch.json 39 | !.vscode/extensions.json 40 | 41 | # dotenv environment variable files 42 | .env 43 | .env.development.local 44 | .env.test.local 45 | .env.production.local 46 | # .env.local 47 | 48 | # temp directory 49 | .temp 50 | .tmp 51 | 52 | # Runtime data 53 | pids 54 | *.pid 55 | *.seed 56 | *.pid.lock 57 | 58 | # Diagnostic reports (https://nodejs.org/api/report.html) 59 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 60 | 61 | .nx -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.19.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribuindo 2 | 3 | Você pode contribuir com o projeto de diversas formas, implementando uma 4 | funcionalidade nova, corrigindo um bug, procurando bugs, revisando pull 5 | requests, entre outras. 6 | Para se inteirar do projeto, entre no 7 | [Discord](https://discord.gg/vjZS6BQXvM) e participe das discussões. 8 | 9 | ## 🤝 Contribuindo com atividades que não são de código 10 | 11 | O projeto precisa de ajuda em diversas frentes diferentes como: QA, produto, 12 | design e gestão. Entre no servidor do Discord onde há canais específicos para 13 | essas atividades. 14 | 15 | Se você encontrou um bug, vá nas 16 | [issues](https://github.com/SOS-RS/backend/issues) 17 | do projeto e reporte-o lá. Verifique antes se já não existe um bug aberto para o 18 | problema que quer relatar, usando a busca. O mesmo vale para novas 19 | funcionalidades. 20 | 21 | O restante deste documento focará nas contribuições de código. 22 | 23 | ## ✅ Escolhendo qual será sua contribuição de código 24 | 25 | Verifique no [projeto do Github](https://github.com/orgs/SOS-RS/projects/1) 26 | quais funcionalidades ainda não foram implementadas e já estão prontas para 27 | serem desenvolvidas, elas estarão na coluna "Disponível pra dev". Lá há itens de 28 | backend e frontend, então atente-se para qual você gostaria de participar. 29 | 30 | Após escolher o item que quer trabalhar, faça um comentário no issue informando 31 | que quer contribuir para sua entrega. Uma pessoa que administra o repositório 32 | marcará você como a pessoa responsável por aquele issue, e marcará o item como 33 | "Em desenvolvimento". 34 | 35 | A partir daí você já pode trabalhar no item que escolheu. 36 | 37 | Você também pode mandar a contribuição diretamente sem avisar, mas corre o 38 | risco de alguém solicitar para trabalhar no item e entregá-lo junto ou antes de 39 | você, desperdiçando assim esforços. Somente faça isso se a correção for bem rápida e pontual para 40 | evitar o desperdício. 41 | 42 | ⚠️ **Importante**: Itens de alta prioridade precisam ser entregues o mais rápido possível, 43 | idealmente em até dois dias. Verifique se tem tempo livre suficiente para se 44 | dedicar a um item de urgência, a fim de evitar segurar o item por tempo demais 45 | de forma desnecessária. 46 | 47 | ## 🚀 Configuração Inicial Local 48 | 49 | 1. Faça um fork do repositório para o seu usuário (uma boa ideia é usar um nome mais descritivo do que `backend`, como `sos-rs-backend`). 50 | 2. Clone o repositório (troque `` na url abaixo pelo seu usuário): 51 | 52 | ```bash 53 | git clone https://github.com//sos-rs-backend.git 54 | ``` 55 | 56 | 3. Faça uma cópia do arquivo `.env`, e altere `DB_HOST=sos-rs-db` para `DB_HOST=localhost`: 57 | 58 | ```bash 59 | sed 's/sos-rs-db/localhost/g' .env.local > .env 60 | # ou copie o arquivo e altere no seu editor preferido 61 | ``` 62 | 63 | 4. Inicie o banco de dados com o Docker (adicione `-d` para rodar em background): 64 | 65 | ```bash 66 | docker compose -f docker-compose.dev.yml up db 67 | # ou em background: 68 | docker compose -f docker-compose.dev.yml up db -d 69 | # para ver os logs: 70 | docker logs sos-rs-db 71 | ``` 72 | 73 | 5. Instale as dependências: 74 | 75 | ```bash 76 | npm install 77 | npx prisma generate 78 | npx prisma migrate dev 79 | ``` 80 | 81 | 6. Inicie o servidor: 82 | 83 | ```bash 84 | npm start 85 | # ou com watch: 86 | npm run start:dev 87 | ``` 88 | 89 | A API estará disponível em . Você poderá acessar o Swagger em . 90 | 91 | 7. Rode os testes: 92 | 93 | ```bash 94 | npm test 95 | # ou com watch: 96 | npm run test:watch 97 | ``` 98 | 99 | ## 💻 Codificando e enviando 100 | 101 | 1. Faça suas alterações. Não deixe de criar os testes. 102 | 2. Rode os testes com `npm test`, feitos com [Jest](https://jestjs.io/). 103 | 3. Rode o lint com `npm run lint`. 104 | 4. Crie um branch com o git `git checkout -b nomedobranch`. 105 | 5. Faça um commit com `git commit`. 106 | 6. Faça um push para o seu repositório com `git push`. 107 | 7. [Sincronize seu repositório](#-sincronizando). 108 | 8. [Abra um pull request](https://docs.github.com/pt/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). 109 | Não deixe de informar, no seu pull request, qual a issue que está fechando. 110 | Idealmente coloque um comentário no PR que já fechará a issue, como 111 | `fixes #xxxx` ou `closes #xxxx` (onde `xxxx` é o número do issue). Veja 112 | [como isso funciona](https://docs.github.com/pt/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests). 113 | 9. Acompanhe a revisão do PR. Algumas verificações automáticas serão feitas (o 114 | Github Actions rodará os testes, por exemplo). Se uma delas falhar, corrija-a, a 115 | revisão humana só começa quando estas checagem estão passando. Após abrir o 116 | PR uma pessoa que administra o projeto pode pedir revisões e alterações. 117 | Busque respondê-las o mais rápido possível para que o PR possa ser integrado. 118 | 119 | ## 🔄 Sincronizando 120 | 121 | Você vai precisar, de tempos em tempos, sincronizar a branch `develop` do 122 | seu repositório. Você pode usar o botão `Sync fork` do Github 123 | (veja [os docs](https://docs.github.com/pt/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)). 124 | Ou você pode fazer manualmente, o que te permite fazer a sincronização sem depender do Github: 125 | 126 | 1. Antes de mais nada, se estiver no meio de uma contribuição, verifique que já commitou 127 | tudo que tinha pra commitar, e então faça checkout do branch `develop`: 128 | 129 | ```bash 130 | git checkout develop 131 | ``` 132 | 133 | 2. Adicione o repositório oficial como remoto com nome `upstream` (só necessário na primeira vez): 134 | 135 | ```bash 136 | git remote add upstream https://github.com/SOS-RS/backend.git 137 | ``` 138 | 139 | 3. Faça pull do branch `develop`: 140 | 141 | ```bash 142 | git pull upstream develop 143 | ``` 144 | 145 | 4. Se estiver no meio de uma contribuição, faça um rebase no branch `develop` 146 | (substitua `` pelo nome do seu branch): 147 | 148 | ```bash 149 | git checkout 150 | git rebase develop 151 | ``` 152 | 153 | Após o rebase, é importante rodar novamente a aplicação e verificar se tudo 154 | continua funcionando, inclusive os testes. 155 | 156 | ## 🗂 Dump do Banco de Dados 157 | 158 | Para iniciar com dados de exemplo, utilize o dump do banco disponível em `prisma/dev_dump.sql`. Este arquivo 159 | pode ser executado após as migrations estarem aplicadas. 160 | 161 | Se estiver usando Docker, os comandos para carregar o dump são: 162 | 163 | ```bash 164 | # Copiar o dump para a pasta temporária do Docker 165 | docker cp prisma/dev_dump.sql sos-rs-db:/tmp/dump.sql 166 | # Importar o dump para o banco 167 | docker exec -i sos-rs-db psql -U root -d sos_rs -f /tmp/dump.sql 168 | ``` 169 | 170 | ## 🐳 Configuração com Docker 171 | 172 | Para desenvolvedores de frontend que não precisam executar localmente a API e o banco, siga estes passos: 173 | 174 | 1. Clone o arquivo `.env` de exemplo: 175 | 176 | ```bash 177 | cp .env.local .env 178 | ``` 179 | 180 | Se você não fizer este passo você precisa adicionar as portas no 181 | `docker-compose.dev.yml` para permitir acessos externos: 182 | 183 | ```yaml 184 | ports: 185 | - '5432:5432' 186 | - '4000:4000' 187 | ``` 188 | 189 | 2. Use o seguinte comando para criar e iniciar o banco via Docker: 190 | 191 | ```bash 192 | docker-compose -f docker-compose.dev.yml up 193 | ``` 194 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.18-alpine as node 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY package.json package-lock.json ./ 6 | 7 | RUN npm install 8 | COPY . . 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SOS-RS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌊 Backend para App de Ajuda em Enchentes 🌊 2 | 3 | Este repositório contém o backend de um aplicativo projetado para ajudar na organização e distribuição de suprimentos, 4 | bem como na coordenação de voluntários durante enchentes no Rio Grande do Sul. Ele fornece APIs essenciais para a 5 | autenticação de usuários, gerenciamento de abrigos e suprimentos, e muito mais. 6 | 7 | Se você quiser discutir ideias, problemas ou contribuições, sinta-se à vontade para se juntar ao nosso servidor do 8 | Discord [aqui](https://discord.gg/vjZS6BQXvM). 9 | 10 | ## 🤝 Contribuição 11 | 12 | Contribuições são muito bem-vindas! Se deseja ajudar, veja o 13 | [documento de contribuição](./CONTRIBUTING.md). 14 | 15 | Sua ajuda é crucial para apoiar a comunidade afetada pelas enchentes no Rio Grande do Sul! 16 | 17 | ## 🛠 Tecnologias Utilizadas 18 | 19 | - **🟢 Node.js**: Ambiente de execução para JavaScript. 20 | - **🔗 Prisma**: ORM para Node.js e TypeScript, facilitando o gerenciamento do banco de dados. 21 | - **🐳 Docker**: Solução para desenvolvimento e execução de aplicativos em contêineres. 22 | - **🐦 Nest**: Framework de alto desempenho para aplicações web em Node.js. 23 | - **📦 PostgreSQL**: Banco de dados relacional robusto e eficiente. 24 | 25 | ## 📡 API Endpoints 26 | 27 | Veja o documento de [endpoints](./docs/endpoints.md). 28 | 29 | ## Licença 30 | 31 | Este código está licenciado usando a 32 | [licença MIT](./LICENSE). 33 | 34 | ## Contribuidores 35 | 36 | Os contribuidores são voluntários, e podem ser encontrados 37 | [na página de contribuidores](https://github.com/SOS-RS/backend/graphs/contributors). 38 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | api: 5 | container_name: sos-rs-api 6 | image: node:18.18-alpine 7 | restart: always 8 | tty: true 9 | depends_on: 10 | - db 11 | ports: 12 | - '${PORT}:${PORT}' 13 | volumes: 14 | - .:/usr/app 15 | - /usr/app/node_modules 16 | working_dir: '/usr/app' 17 | environment: 18 | - DB_HOST=${DB_HOST} 19 | - DB_PORT=${DB_PORT} 20 | - DB_DATABASE_NAME=${DB_DATABASE_NAME} 21 | - DB_USER=${DB_USER} 22 | - DB_PASSWORD=${DB_PASSWORD} 23 | - PORT=${PORT} 24 | command: > 25 | sh -c "npm install && 26 | npx prisma generate && 27 | npx prisma migrate dev && 28 | npm run start:dev -- --preserveWatchOutput" 29 | db: 30 | container_name: sos-rs-db 31 | image: postgres 32 | ports: 33 | - '${DB_PORT}:${DB_PORT}' 34 | environment: 35 | - POSTGRES_PASSWORD=${DB_PASSWORD} 36 | - POSTGRES_USER=${DB_USER} 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sos-rs-api: 3 | container_name: sos-rs-api 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | restart: always 8 | tty: true 9 | ports: 10 | - '4000:4000' 11 | command: > 12 | sh -c "npx prisma generate && 13 | npx prisma migrate deploy && 14 | npm run build && npm run start:prod" 15 | -------------------------------------------------------------------------------- /docs/endpoints.md: -------------------------------------------------------------------------------- 1 | # 📡 API Endpoints 2 | 3 | ## 🧑‍💻 Usuários 4 | 5 | - **📝 POST /users** - Registra um novo usuário. 6 | - **🔧 PUT /users** - Atualiza um usuário existente. 7 | 8 | ## 🚪 Sessões 9 | 10 | - **📝 POST /sessions** - Inicia uma nova sessão de usuário. 11 | - **👀 GET /sessions/:sessionId** - Retorna detalhes de uma sessão. 12 | - **🔧 PUT /sessions/:sessionId** - Atualiza uma sessão. 13 | 14 | ## 🏠 Abrigos 15 | 16 | - **📝 POST /shelters** - Registra um novo abrigo. 17 | - **🔧 PUT /shelters/:shelterId** - Atualiza um abrigo. 18 | - **👀 GET /shelters** - Lista abrigos. 19 | 20 | ## 📦 Suprimentos 21 | 22 | - **📝 POST /supply** - Registra um novo item de suprimento. 23 | - **🔧 PUT /supplies/:supplyId** - Atualiza um suprimento. 24 | - **👀 GET /supplies** - Lista suprimentos. 25 | 26 | ## 🏷️ Categorias de Suprimentos 27 | 28 | - **📝 POST /supply-categories** - Registra uma nova categoria de suprimentos. 29 | - **🔧 PUT /supply-categories/:categoryId** - Atualiza uma categoria. 30 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | rootDir: 'src', 6 | testRegex: '.*\\.spec\\.ts$', 7 | transform: { 8 | '^.+\\.(t|j)s$': 'ts-jest', 9 | }, 10 | collectCoverageFrom: ['**/*.(t|j)s'], 11 | coverageDirectory: '../coverage', 12 | moduleNameMapper: { 13 | '^src/(.*)$': '/$1', 14 | '^@/(.*)$': '/$1', 15 | '^test/(.*)$': '/../$1', 16 | }, 17 | testEnvironment: 'node', 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/src/main.js", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "lint:ci": "eslint \"{src,apps,libs,test}/**/*.ts\" --format=stylish", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "pretest:e2e": "dotenvx run --overload -f .env.local -f .env.test -- prisma migrate deploy", 22 | "test:e2e": "dotenvx run --overload -f .env.local -f .env.test -- jest --config ./test/jest.e2e.config.ts", 23 | "migrations:run": "prisma migrate deploy", 24 | "migrations:dev": "prisma migrate dev", 25 | "docker:compose": "docker-compose -f docker-compose.dev.yml up" 26 | }, 27 | "dependencies": { 28 | "@fastify/static": "^7.0.3", 29 | "@nestjs/common": "^10.0.0", 30 | "@nestjs/core": "^10.0.0", 31 | "@nestjs/jwt": "^10.2.0", 32 | "@nestjs/passport": "^10.0.3", 33 | "@nestjs/platform-fastify": "^10.3.8", 34 | "@nestjs/schedule": "^4.0.2", 35 | "@nestjs/swagger": "^7.3.1", 36 | "@prisma/client": "^5.13.0", 37 | "bcrypt": "^5.1.1", 38 | "date-fns": "^3.6.0", 39 | "fastify": "^4.27.0", 40 | "passport-jwt": "^4.0.1", 41 | "reflect-metadata": "^0.2.0", 42 | "rxjs": "^7.8.1", 43 | "zod": "^3.23.6" 44 | }, 45 | "devDependencies": { 46 | "@dotenvx/dotenvx": "^0.44.0", 47 | "@nestjs/cli": "^10.0.0", 48 | "@nestjs/schematics": "^10.0.0", 49 | "@nestjs/testing": "^10.0.0", 50 | "@types/jest": "^29.5.2", 51 | "@types/node": "^20.3.1", 52 | "@types/query-string": "^6.3.0", 53 | "@types/supertest": "^6.0.0", 54 | "@typescript-eslint/eslint-plugin": "^6.0.0", 55 | "@typescript-eslint/parser": "^6.0.0", 56 | "eslint": "^8.42.0", 57 | "eslint-config-prettier": "^9.0.0", 58 | "eslint-plugin-jest": "^28.5.0", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "jest": "^29.5.0", 61 | "prettier": "^3.0.0", 62 | "prisma": "^5.13.0", 63 | "source-map-support": "^0.5.21", 64 | "supertest": "^6.3.3", 65 | "ts-jest": "^29.1.0", 66 | "ts-loader": "^9.4.3", 67 | "ts-node": "^10.9.1", 68 | "tsconfig-paths": "^4.2.0", 69 | "typescript": "^5.1.3" 70 | }, 71 | "engines": { 72 | "node": ">=18.18", 73 | "npm": ">=10.5.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /prisma/migrations/20240505182421_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "SupplyStatus" AS ENUM ('UnderControl', 'Remaining', 'Needing', 'Urgent'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "users" ( 6 | "id" TEXT NOT NULL, 7 | "name" TEXT NOT NULL, 8 | "login" TEXT NOT NULL, 9 | "password" TEXT NOT NULL, 10 | "phone" TEXT NOT NULL, 11 | "created_at" VARCHAR(32) NOT NULL, 12 | "updated_at" VARCHAR(32), 13 | 14 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateTable 18 | CREATE TABLE "category_supplies" ( 19 | "id" TEXT NOT NULL, 20 | "name" TEXT NOT NULL, 21 | "created_at" VARCHAR(32) NOT NULL, 22 | "updated_at" VARCHAR(32), 23 | 24 | CONSTRAINT "category_supplies_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateTable 28 | CREATE TABLE "supplies" ( 29 | "id" TEXT NOT NULL, 30 | "shelter_id" TEXT NOT NULL, 31 | "supply_category_id" TEXT NOT NULL, 32 | "name" TEXT NOT NULL, 33 | "status" "SupplyStatus" NOT NULL, 34 | "created_at" VARCHAR(32) NOT NULL, 35 | "updated_at" VARCHAR(32), 36 | 37 | CONSTRAINT "supplies_pkey" PRIMARY KEY ("id") 38 | ); 39 | 40 | -- CreateTable 41 | CREATE TABLE "shelters" ( 42 | "id" TEXT NOT NULL, 43 | "pix" TEXT NOT NULL, 44 | "address" TEXT NOT NULL, 45 | "pet_friendly" BOOLEAN NOT NULL, 46 | "sheltered_people" INTEGER NOT NULL, 47 | "capacity" INTEGER, 48 | "contact" TEXT NOT NULL, 49 | "created_at" VARCHAR(32) NOT NULL, 50 | "updated_at" VARCHAR(32), 51 | 52 | CONSTRAINT "shelters_pkey" PRIMARY KEY ("id") 53 | ); 54 | 55 | -- CreateIndex 56 | CREATE UNIQUE INDEX "users_login_key" ON "users"("login"); 57 | 58 | -- CreateIndex 59 | CREATE UNIQUE INDEX "users_phone_key" ON "users"("phone"); 60 | 61 | -- CreateIndex 62 | CREATE UNIQUE INDEX "shelters_pix_key" ON "shelters"("pix"); 63 | 64 | -- AddForeignKey 65 | ALTER TABLE "supplies" ADD CONSTRAINT "supplies_supply_category_id_fkey" FOREIGN KEY ("supply_category_id") REFERENCES "category_supplies"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 66 | 67 | -- AddForeignKey 68 | ALTER TABLE "supplies" ADD CONSTRAINT "supplies_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 69 | -------------------------------------------------------------------------------- /prisma/migrations/20240505185353_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "AccessLevel" AS ENUM ('User', 'Staff'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "users" ADD COLUMN "accessLevel" "AccessLevel" NOT NULL DEFAULT 'User'; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240505185534_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "sessions" ( 3 | "id" TEXT NOT NULL, 4 | "user_id" TEXT NOT NULL, 5 | "ip" TEXT, 6 | "user_agent" TEXT, 7 | "active" BOOLEAN NOT NULL DEFAULT true, 8 | "created_at" VARCHAR(32) NOT NULL, 9 | "updated_at" VARCHAR(32), 10 | 11 | CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20240505190155_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "supplies" ALTER COLUMN "status" SET DEFAULT 'UnderControl'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240505191728_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ALTER COLUMN "pet_friendly" DROP NOT NULL, 3 | ALTER COLUMN "sheltered_people" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240505201711_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `lastName` to the `users` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "users" ADD COLUMN "lastName" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240505203201_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `shelters` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `name` to the `shelters` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "shelters" ADD COLUMN "name" TEXT NOT NULL; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "shelters_name_key" ON "shelters"("name"); 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240505222048_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `category_supplies` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "shelters" ALTER COLUMN "pix" DROP NOT NULL; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "category_supplies_name_key" ON "category_supplies"("name"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240505222318_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ALTER COLUMN "contact" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240505224712_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `supplies` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "supplies_name_key" ON "supplies"("name"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240505225104_20240505222318/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "supplies_name_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240506015214_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `status` on the `supplies` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "supplies" DROP COLUMN "status", 9 | ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0; 10 | 11 | -- DropEnum 12 | DROP TYPE "SupplyStatus"; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240506021537_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ADD COLUMN "priority_sum" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240506195247_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "shelter_managers" ( 3 | "shelter_id" TEXT NOT NULL, 4 | "user_id" TEXT NOT NULL, 5 | "created_at" VARCHAR(32) NOT NULL, 6 | "updated_at" VARCHAR(32), 7 | 8 | CONSTRAINT "shelter_managers_pkey" PRIMARY KEY ("shelter_id","user_id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "shelter_managers" ADD CONSTRAINT "shelter_managers_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "shelter_managers" ADD CONSTRAINT "shelter_managers_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20240507205058_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "shelter_supplies" ( 3 | "shelter_id" TEXT NOT NULL, 4 | "supply_id" TEXT NOT NULL, 5 | "priority" INTEGER NOT NULL, 6 | "created_at" VARCHAR(32) NOT NULL, 7 | "updated_at" VARCHAR(32), 8 | 9 | CONSTRAINT "shelter_supplies_pkey" PRIMARY KEY ("shelter_id","supply_id") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "shelter_supplies" ADD CONSTRAINT "shelter_supplies_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "shelter_supplies" ADD CONSTRAINT "shelter_supplies_supply_id_fkey" FOREIGN KEY ("supply_id") REFERENCES "supplies"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20240507221950_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ADD COLUMN "latitude" DOUBLE PRECISION, 3 | ADD COLUMN "longitude" DOUBLE PRECISION; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240508022250_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `shelter_id` on the `supplies` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "supplies" DROP CONSTRAINT "supplies_shelter_id_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "supplies" DROP COLUMN "shelter_id"; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240508041443_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `priority` on the `supplies` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "shelter_supplies" ALTER COLUMN "priority" SET DEFAULT 0; 9 | 10 | -- AlterTable 11 | ALTER TABLE "supplies" DROP COLUMN "priority"; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240508050213_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `supplies` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "supplies_name_key" ON "supplies"("name"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240508150340_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "supplies_name_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240508193500_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ADD COLUMN "verified" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240509235041_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelter_supplies" ADD COLUMN "quantity" INTEGER; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240510225124_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "AccessLevel" ADD VALUE 'DistributionCenter'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240511054108_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "AccessLevel" ADD VALUE 'Admin'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240512005246_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ADD COLUMN "city" TEXT, 3 | ADD COLUMN "neighbourhood" TEXT, 4 | ADD COLUMN "street" TEXT, 5 | ADD COLUMN "street_number" TEXT, 6 | ADD COLUMN "zip_code" TEXT; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20240514211840_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "partners" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "link" TEXT NOT NULL, 6 | "iconName" TEXT NOT NULL DEFAULT 'Handshake', 7 | "created_at" VARCHAR(32) NOT NULL, 8 | "updated_at" VARCHAR(32), 9 | 10 | CONSTRAINT "partners_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "partners_name_key" ON "partners"("name"); 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240515003750_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `iconName` on the `partners` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "partners" DROP COLUMN "iconName"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240516140110_add_capacity_and_shelter_pets/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "shelters" ADD COLUMN "pets_capacity" INTEGER, 3 | ADD COLUMN "sheltered_pets" INTEGER; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240516212629_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "supporters" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "image_url" TEXT NOT NULL, 6 | "link" TEXT NOT NULL, 7 | "created_at" VARCHAR(32) NOT NULL, 8 | "updated_at" VARCHAR(32), 9 | 10 | CONSTRAINT "supporters_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "supporters_name_key" ON "supporters"("name"); 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240517181431_add_unaccent_extension/migration.sql: -------------------------------------------------------------------------------- 1 | -- This is an empty migration. 2 | 3 | CREATE EXTENSION IF NOT EXISTS unaccent; -------------------------------------------------------------------------------- /prisma/migrations/20240517192040_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ShelterCategory" AS ENUM ('Shelter', 'DistributionCenter'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "shelters" ADD COLUMN "actived" BOOLEAN NOT NULL DEFAULT true, 6 | ADD COLUMN "category" "ShelterCategory" NOT NULL DEFAULT 'Shelter'; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20240522191544_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "supplies_history" ( 3 | "id" TEXT NOT NULL, 4 | "shelter_id" TEXT NOT NULL, 5 | "supply_id" TEXT NOT NULL, 6 | "priority" INTEGER, 7 | "quantity" INTEGER, 8 | "created_at" VARCHAR(32) NOT NULL, 9 | 10 | CONSTRAINT "supplies_history_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "supplies_history_shelter_id_supply_id_idx" ON "supplies_history"("shelter_id", "supply_id"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_shelter_id_supply_id_fkey" FOREIGN KEY ("shelter_id", "supply_id") REFERENCES "shelter_supplies"("shelter_id", "supply_id") ON DELETE RESTRICT ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20240522195717_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[sucessor_id]` on the table `supplies_history` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "supplies_history" DROP CONSTRAINT "supplies_history_shelter_id_supply_id_fkey"; 9 | 10 | -- DropIndex 11 | DROP INDEX "supplies_history_shelter_id_supply_id_idx"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "supplies_history" ADD COLUMN "sucessor_id" TEXT; 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "supplies_history_sucessor_id_key" ON "supplies_history"("sucessor_id"); 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_sucessor_id_fkey" FOREIGN KEY ("sucessor_id") REFERENCES "supplies_history"("id") ON DELETE SET NULL ON UPDATE CASCADE; 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_supply_id_fkey" FOREIGN KEY ("supply_id") REFERENCES "supplies"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /prisma/migrations/20240522200429_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `sucessor_id` on the `supplies_history` table. All the data in the column will be lost. 5 | - A unique constraint covering the columns `[predecessor_id]` on the table `supplies_history` will be added. If there are existing duplicate values, this will fail. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "supplies_history" DROP CONSTRAINT "supplies_history_sucessor_id_fkey"; 10 | 11 | -- DropIndex 12 | DROP INDEX "supplies_history_sucessor_id_key"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "supplies_history" DROP COLUMN "sucessor_id", 16 | ADD COLUMN "predecessor_id" TEXT; 17 | 18 | -- CreateIndex 19 | CREATE UNIQUE INDEX "supplies_history_predecessor_id_key" ON "supplies_history"("predecessor_id"); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_predecessor_id_fkey" FOREIGN KEY ("predecessor_id") REFERENCES "supplies_history"("id") ON DELETE SET NULL ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240522220544_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "supplies_history" ADD COLUMN "ip" TEXT, 3 | ADD COLUMN "user_agent" TEXT; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240523211341_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "supplies_history" ADD COLUMN "user_id" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "supplies_history" ADD CONSTRAINT "supplies_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240528182902_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "SupplyMeasure" AS ENUM ('Unit', 'Kg', 'Litters', 'Box', 'Piece'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "DonationOrderStatus" AS ENUM ('Pending', 'Canceled', 'Complete'); 6 | 7 | -- AlterTable 8 | ALTER TABLE "supplies" ADD COLUMN "measure" "SupplyMeasure" NOT NULL DEFAULT 'Unit'; 9 | 10 | -- CreateTable 11 | CREATE TABLE "donation_order_supplies" ( 12 | "id" TEXT NOT NULL, 13 | "donation_order_id" TEXT NOT NULL, 14 | "supply_id" TEXT NOT NULL, 15 | "quantity" INTEGER NOT NULL, 16 | "created_at" VARCHAR(32) NOT NULL, 17 | "updated_at" VARCHAR(32), 18 | 19 | CONSTRAINT "donation_order_supplies_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "donation_orders" ( 24 | "id" TEXT NOT NULL, 25 | "user_id" TEXT NOT NULL, 26 | "shelter_id" TEXT NOT NULL, 27 | "status" "DonationOrderStatus" NOT NULL DEFAULT 'Pending', 28 | "created_at" VARCHAR(32) NOT NULL, 29 | "updated_at" VARCHAR(32), 30 | 31 | CONSTRAINT "donation_orders_pkey" PRIMARY KEY ("id") 32 | ); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "donation_order_supplies" ADD CONSTRAINT "donation_order_supplies_supply_id_fkey" FOREIGN KEY ("supply_id") REFERENCES "supplies"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 36 | 37 | -- AddForeignKey 38 | ALTER TABLE "donation_order_supplies" ADD CONSTRAINT "donation_order_supplies_donation_order_id_fkey" FOREIGN KEY ("donation_order_id") REFERENCES "donation_orders"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 39 | 40 | -- AddForeignKey 41 | ALTER TABLE "donation_orders" ADD CONSTRAINT "donation_orders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 42 | 43 | -- AddForeignKey 44 | ALTER TABLE "donation_orders" ADD CONSTRAINT "donation_orders_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 45 | -------------------------------------------------------------------------------- /prisma/migrations/20240605140756_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "shelter_users" ( 3 | "user_id" TEXT NOT NULL, 4 | "shelter_id" TEXT NOT NULL, 5 | "created_at" VARCHAR(32) NOT NULL, 6 | 7 | CONSTRAINT "shelter_users_pkey" PRIMARY KEY ("user_id","shelter_id") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "shelter_users" ADD CONSTRAINT "shelter_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "shelter_users" ADD CONSTRAINT "shelter_users_shelter_id_fkey" FOREIGN KEY ("shelter_id") REFERENCES "shelters"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | enum AccessLevel { 11 | User 12 | Staff 13 | DistributionCenter 14 | Admin 15 | } 16 | 17 | enum ShelterCategory { 18 | Shelter 19 | DistributionCenter 20 | } 21 | 22 | enum SupplyMeasure { 23 | Unit 24 | Kg 25 | Litters 26 | Box 27 | Piece 28 | } 29 | 30 | enum DonationOrderStatus { 31 | Pending 32 | Canceled 33 | Complete 34 | } 35 | 36 | model User { 37 | id String @id @default(uuid()) 38 | name String 39 | lastName String 40 | login String @unique 41 | password String 42 | phone String @unique 43 | accessLevel AccessLevel @default(value: User) 44 | createdAt String @map("created_at") @db.VarChar(32) 45 | updatedAt String? @map("updated_at") @db.VarChar(32) 46 | 47 | sessions Session[] 48 | shelterManagers ShelterManagers[] 49 | suppliesHistory SupplyHistory[] 50 | donationOrders DonationOrder[] 51 | shelterUsers ShelterUsers[] 52 | 53 | @@map("users") 54 | } 55 | 56 | model Session { 57 | id String @id @default(uuid()) 58 | userId String @map("user_id") 59 | ip String? 60 | userAgent String? @map("user_agent") 61 | active Boolean @default(value: true) 62 | createdAt String @map("created_at") @db.VarChar(32) 63 | updatedAt String? @map("updated_at") @db.VarChar(32) 64 | 65 | user User @relation(fields: [userId], references: [id]) 66 | 67 | @@map("sessions") 68 | } 69 | 70 | model SupplyCategory { 71 | id String @id @default(uuid()) 72 | name String @unique 73 | createdAt String @map("created_at") @db.VarChar(32) 74 | updatedAt String? @map("updated_at") @db.VarChar(32) 75 | 76 | supplies Supply[] 77 | 78 | @@map("category_supplies") 79 | } 80 | 81 | model ShelterSupply { 82 | shelterId String @map("shelter_id") 83 | supplyId String @map("supply_id") 84 | priority Int @default(value: 0) 85 | quantity Int? 86 | createdAt String @map("created_at") @db.VarChar(32) 87 | updatedAt String? @map("updated_at") @db.VarChar(32) 88 | 89 | shelter Shelter @relation(fields: [shelterId], references: [id]) 90 | supply Supply @relation(fields: [supplyId], references: [id]) 91 | 92 | @@id([shelterId, supplyId]) 93 | @@map("shelter_supplies") 94 | } 95 | 96 | model Supply { 97 | id String @id @default(uuid()) 98 | supplyCategoryId String @map("supply_category_id") 99 | name String 100 | measure SupplyMeasure @default(value: Unit) 101 | createdAt String @map("created_at") @db.VarChar(32) 102 | updatedAt String? @map("updated_at") @db.VarChar(32) 103 | 104 | supplyCategory SupplyCategory @relation(fields: [supplyCategoryId], references: [id]) 105 | shelterSupplies ShelterSupply[] 106 | supplyHistories SupplyHistory[] 107 | DonationOrderSupply DonationOrderSupply[] 108 | 109 | @@map("supplies") 110 | } 111 | 112 | model Shelter { 113 | id String @id @default(uuid()) 114 | name String @unique 115 | pix String? @unique 116 | address String 117 | street String? 118 | neighbourhood String? 119 | city String? 120 | streetNumber String? @map("street_number") 121 | zipCode String? @map("zip_code") 122 | petFriendly Boolean? @map("pet_friendly") 123 | shelteredPets Int? @map("sheltered_pets") 124 | petsCapacity Int? @map("pets_capacity") 125 | shelteredPeople Int? @map("sheltered_people") 126 | capacity Int? 127 | contact String? 128 | prioritySum Int @default(value: 0) @map("priority_sum") 129 | latitude Float? 130 | longitude Float? 131 | verified Boolean @default(value: false) 132 | category ShelterCategory @default(value: Shelter) 133 | actived Boolean @default(value: true) 134 | createdAt String @map("created_at") @db.VarChar(32) 135 | updatedAt String? @map("updated_at") @db.VarChar(32) 136 | 137 | shelterManagers ShelterManagers[] 138 | shelterSupplies ShelterSupply[] 139 | supplyHistories SupplyHistory[] 140 | donationOrders DonationOrder[] 141 | shelterUsers ShelterUsers[] 142 | 143 | @@map("shelters") 144 | } 145 | 146 | model ShelterManagers { 147 | shelterId String @map("shelter_id") 148 | userId String @map("user_id") 149 | createdAt String @map("created_at") @db.VarChar(32) 150 | updatedAt String? @map("updated_at") @db.VarChar(32) 151 | 152 | user User @relation(fields: [userId], references: [id]) 153 | shelter Shelter @relation(fields: [shelterId], references: [id]) 154 | 155 | @@id([shelterId, userId]) 156 | @@map("shelter_managers") 157 | } 158 | 159 | model Partners { 160 | id String @id @default(uuid()) 161 | name String @unique 162 | link String 163 | createdAt String @map("created_at") @db.VarChar(32) 164 | updatedAt String? @map("updated_at") @db.VarChar(32) 165 | 166 | @@map("partners") 167 | } 168 | 169 | model Supporters { 170 | id String @id @default(uuid()) 171 | name String @unique 172 | imageUrl String @map("image_url") 173 | link String 174 | createdAt String @map("created_at") @db.VarChar(32) 175 | updatedAt String? @map("updated_at") @db.VarChar(32) 176 | 177 | @@map("supporters") 178 | } 179 | 180 | model SupplyHistory { 181 | id String @id @default(uuid()) 182 | predecessorId String? @unique @map("predecessor_id") 183 | shelterId String @map("shelter_id") 184 | supplyId String @map("supply_id") 185 | userId String? @map("user_id") 186 | priority Int? 187 | quantity Int? 188 | ip String? 189 | userAgent String? @map("user_agent") 190 | createdAt String @map("created_at") @db.VarChar(32) 191 | 192 | predecessor SupplyHistory? @relation("supplyHistory", fields: [predecessorId], references: [id]) 193 | successor SupplyHistory? @relation("supplyHistory") 194 | shelter Shelter @relation(fields: [shelterId], references: [id]) 195 | supply Supply @relation(fields: [supplyId], references: [id]) 196 | user User? @relation(fields: [userId], references: [id]) 197 | 198 | @@map("supplies_history") 199 | } 200 | 201 | model DonationOrderSupply { 202 | id String @id @default(uuid()) 203 | donationOrderId String @map("donation_order_id") 204 | supplyId String @map("supply_id") 205 | quantity Int 206 | createdAt String @map("created_at") @db.VarChar(32) 207 | updatedAt String? @map("updated_at") @db.VarChar(32) 208 | 209 | supply Supply @relation(fields: [supplyId], references: [id]) 210 | donationOrder DonationOrder @relation(fields: [donationOrderId], references: [id]) 211 | 212 | @@map("donation_order_supplies") 213 | } 214 | 215 | model DonationOrder { 216 | id String @id @default(uuid()) 217 | userId String @map("user_id") 218 | shelterId String @map("shelter_id") 219 | status DonationOrderStatus @default(value: Pending) 220 | createdAt String @map("created_at") @db.VarChar(32) 221 | updatedAt String? @map("updated_at") @db.VarChar(32) 222 | 223 | user User @relation(fields: [userId], references: [id]) 224 | shelter Shelter @relation(fields: [shelterId], references: [id]) 225 | donationOrderSupplies DonationOrderSupply[] 226 | 227 | @@map("donation_orders") 228 | } 229 | 230 | model ShelterUsers { 231 | userId String @map("user_id") 232 | shelterId String @map("shelter_id") 233 | createdAt String @map("created_at") @db.VarChar(32) 234 | 235 | user User @relation(fields: [userId], references: [id]) 236 | shelter Shelter @relation(fields: [shelterId], references: [id]) 237 | 238 | @@id([userId, shelterId]) 239 | @@map("shelter_users") 240 | } 241 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 2 | import { APP_INTERCEPTOR } from '@nestjs/core'; 3 | 4 | import { PrismaModule } from './prisma/prisma.module'; 5 | import { ShelterModule } from './shelter/shelter.module'; 6 | import { SupplyModule } from './supply/supply.module'; 7 | import { ServerResponseInterceptor } from './interceptors'; 8 | import { LoggingMiddleware } from './middlewares/logging.middleware'; 9 | import { UsersModule } from './users/users.module'; 10 | import { SessionsModule } from './sessions/sessions.module'; 11 | import { SupplyCategoriesModule } from './supply-categories/supply-categories.module'; 12 | import { ShelterManagersModule } from './shelter-managers/shelter-managers.module'; 13 | import { ShelterSupplyModule } from './shelter-supply/shelter-supply.module'; 14 | import { PartnersModule } from './partners/partners.module'; 15 | import { DashboardModule } from './dashboard/dashboard.module'; 16 | import { SupportersModule } from './supporters/supporters.module'; 17 | import { SuppliesHistoryModule } from './supplies-history/supplies-history.module'; 18 | import { DonationOrderModule } from './donation-order/donation-order.module'; 19 | 20 | @Module({ 21 | imports: [ 22 | PrismaModule, 23 | UsersModule, 24 | SessionsModule, 25 | ShelterModule, 26 | SupplyModule, 27 | SupplyCategoriesModule, 28 | ShelterManagersModule, 29 | ShelterSupplyModule, 30 | PartnersModule, 31 | DashboardModule, 32 | SupportersModule, 33 | SuppliesHistoryModule, 34 | DonationOrderModule, 35 | ], 36 | controllers: [], 37 | providers: [ 38 | { 39 | provide: APP_INTERCEPTOR, 40 | useClass: ServerResponseInterceptor, 41 | }, 42 | ], 43 | }) 44 | export class AppModule implements NestModule { 45 | configure(consumer: MiddlewareConsumer) { 46 | consumer.apply(LoggingMiddleware).forRoutes('*'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { DashboardController } from './dashboard.controller'; 4 | import { DashboardService } from './dashboard.service'; 5 | import { PrismaService } from '../prisma/prisma.service'; 6 | import { PrismaModule } from '../prisma/prisma.module'; 7 | 8 | describe('DashboardController', () => { 9 | let controller: DashboardController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [PrismaModule], 14 | controllers: [DashboardController], 15 | providers: [DashboardService], 16 | }) 17 | .useMocker((token) => { 18 | if (token === PrismaService) { 19 | return {}; 20 | } 21 | }) 22 | .compile(); 23 | 24 | controller = module.get(DashboardController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpException, Logger, Query } from '@nestjs/common'; 2 | import { DashboardService } from './dashboard.service'; 3 | import { ServerResponse } from '@/utils/utils'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | 6 | @ApiTags('Dashboard') 7 | @Controller('dashboard') 8 | export class DashboardController { 9 | private logger = new Logger(); 10 | constructor(private readonly dashboardService: DashboardService) {} 11 | 12 | @Get('') 13 | async index(@Query() query) { 14 | try { 15 | const data = await this.dashboardService.index(query); 16 | return new ServerResponse(200, 'Successfully get dashboard', data); 17 | } catch (err: any) { 18 | this.logger.error(`Failed to get shelters: ${err}`); 19 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DashboardService } from './dashboard.service'; 3 | import { DashboardController } from './dashboard.controller'; 4 | import { PrismaModule } from 'src/prisma/prisma.module'; 5 | 6 | @Module({ 7 | imports: [PrismaModule], 8 | controllers: [DashboardController], 9 | providers: [DashboardService], 10 | }) 11 | export class DashboardModule {} 12 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { DashboardService } from './dashboard.service'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | describe('DashboardService', () => { 8 | let service: DashboardService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [PrismaModule], 13 | providers: [DashboardService], 14 | }) 15 | .useMocker((token) => { 16 | if (token === PrismaService) { 17 | return {}; 18 | } 19 | }) 20 | .compile(); 21 | 22 | service = module.get(DashboardService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/dashboard/dashboard.service.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { ShelterSearchPropsSchema } from 'src/shelter/types/search.types'; 5 | import { SearchSchema } from 'src/types'; 6 | import { ShelterSearch } from 'src/shelter/ShelterSearch'; 7 | import { DefaultArgs } from '@prisma/client/runtime/library'; 8 | import { Prisma } from '@prisma/client'; 9 | 10 | @Injectable() 11 | export class DashboardService { 12 | constructor(private readonly prismaService: PrismaService) {} 13 | 14 | async index(query: any) { 15 | const { 16 | order, 17 | orderBy, 18 | page, 19 | perPage, 20 | search: searchQuery, 21 | } = SearchSchema.parse(query); 22 | const queryData = ShelterSearchPropsSchema.parse(qs.parse(searchQuery)); 23 | const { getQuery } = new ShelterSearch(this.prismaService, queryData); 24 | const where = await getQuery(); 25 | 26 | const take = perPage; 27 | const skip = perPage * (page - 1); 28 | 29 | const whereData: Prisma.ShelterFindManyArgs = { 30 | take, 31 | skip, 32 | orderBy: { [orderBy]: order }, 33 | where, 34 | }; 35 | 36 | const allShelters = await this.prismaService.shelter.findMany({ 37 | ...whereData, 38 | select: { 39 | id: true, 40 | name: true, 41 | shelteredPeople: true, 42 | actived: true, 43 | capacity: true, 44 | shelterSupplies: { 45 | select: { 46 | priority: true, 47 | supply: { 48 | select: { 49 | measure: true, 50 | supplyCategory: { 51 | select: { 52 | name: true, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }); 61 | 62 | const categoriesWithPriorities = 63 | await this.prismaService.supplyCategory.findMany({ 64 | select: { 65 | id: true, 66 | name: true, 67 | supplies: { 68 | select: { 69 | shelterSupplies: { 70 | select: { 71 | priority: true, 72 | shelterId: true, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }); 79 | 80 | const result = categoriesWithPriorities.map((category) => { 81 | const priorityCounts = { 82 | priority100: 0, 83 | priority10: 0, 84 | priority1: 0, 85 | }; 86 | 87 | const countedShelters = new Set(); 88 | 89 | category.supplies.forEach((supply) => { 90 | supply.shelterSupplies.forEach((shelterSupply) => { 91 | if (!countedShelters.has(shelterSupply.shelterId)) { 92 | switch (shelterSupply.priority) { 93 | case 100: 94 | priorityCounts.priority100++; 95 | break; 96 | case 10: 97 | priorityCounts.priority10++; 98 | break; 99 | case 1: 100 | priorityCounts.priority1++; 101 | break; 102 | default: 103 | break; 104 | } 105 | countedShelters.add(shelterSupply.shelterId); 106 | } 107 | }); 108 | }); 109 | 110 | return { 111 | categoryId: category.id, 112 | categoryName: category.name, 113 | ...priorityCounts, 114 | }; 115 | }); 116 | 117 | const allPeopleSheltered = allShelters.reduce((accumulator, current) => { 118 | if ( 119 | current.actived && 120 | current.capacity !== null && 121 | current.capacity > 0 122 | ) { 123 | return accumulator + (current.shelteredPeople ?? 0); 124 | } else { 125 | return accumulator; 126 | } 127 | }, 0); 128 | 129 | const numSheltersAvailable = allShelters.filter((shelter) => { 130 | if ( 131 | shelter.actived && 132 | shelter.capacity !== null && 133 | shelter.capacity > 0 134 | ) { 135 | return (shelter.shelteredPeople ?? 0) < shelter.capacity; 136 | } 137 | return false; 138 | }).length; 139 | 140 | const numSheltersFull = allShelters.reduce((count, shelter) => { 141 | if ( 142 | shelter.actived && 143 | shelter.capacity !== null && 144 | shelter.capacity > 0 145 | ) { 146 | if ((shelter.shelteredPeople ?? 0) >= shelter.capacity) { 147 | return count + 1; 148 | } 149 | } 150 | return count; 151 | }, 0); 152 | 153 | const shelterWithoutInformation = allShelters.reduce((count, shelter) => { 154 | if ( 155 | shelter.shelteredPeople === null || 156 | shelter.shelteredPeople === undefined 157 | ) { 158 | return count + 1; 159 | } 160 | return count; 161 | }, 0); 162 | 163 | return { 164 | allShelters: allShelters.length, 165 | allPeopleSheltered: allPeopleSheltered, 166 | shelterAvaliable: numSheltersAvailable, 167 | shelterFull: numSheltersFull, 168 | shelterWithoutInformation: shelterWithoutInformation, 169 | categoriesWithPriorities: result, 170 | }; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/decorators/Hmac/hmac.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseGuards } from '@nestjs/common'; 2 | 3 | import { HmacGuard } from '@/guards/hmac.guard'; 4 | 5 | export function Hmac() { 6 | return applyDecorators(UseGuards(HmacGuard)); 7 | } 8 | -------------------------------------------------------------------------------- /src/decorators/Hmac/index.ts: -------------------------------------------------------------------------------- 1 | import { Hmac } from './hmac.decorator'; 2 | 3 | export { Hmac }; 4 | -------------------------------------------------------------------------------- /src/decorators/RegisterShelterSupplyHistory/RegisterShelterSupplyHistory.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseInterceptors } from '@nestjs/common'; 2 | 3 | import { ShelterSupplyHistoryAction } from '@/interceptors/interceptors/shelter-supply-history/types'; 4 | import { ShelterSupplyHistoryInterceptor } from '@/interceptors/interceptors'; 5 | 6 | export function RegisterShelterSupplyHistory( 7 | action: ShelterSupplyHistoryAction, 8 | ) { 9 | return applyDecorators( 10 | UseInterceptors(new ShelterSupplyHistoryInterceptor(action)), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/decorators/RegisterShelterSupplyHistory/index.ts: -------------------------------------------------------------------------------- 1 | import { RegisterShelterSupplyHistory } from './RegisterShelterSupplyHistory.decorator'; 2 | 3 | export { RegisterShelterSupplyHistory }; 4 | -------------------------------------------------------------------------------- /src/decorators/UserDecorator/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const UserDecorator = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request?.user; 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/donation-order/donation-order.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { DonationOrderController } from './donation-order.controller'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | import { DonationOrderService } from './donation-order.service'; 7 | 8 | describe('DonationOrderController', () => { 9 | let controller: DonationOrderController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [PrismaModule], 14 | providers: [DonationOrderService], 15 | controllers: [DonationOrderController], 16 | }) 17 | .useMocker((token) => { 18 | if (token === PrismaService) { 19 | return {}; 20 | } 21 | }) 22 | .compile(); 23 | 24 | controller = module.get(DonationOrderController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/donation-order/donation-order.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | Request, 12 | UseGuards, 13 | } from '@nestjs/common'; 14 | import { ApiTags } from '@nestjs/swagger'; 15 | 16 | import { DonationOrderService } from './donation-order.service'; 17 | import { ServerResponse } from '../utils'; 18 | import { UserGuard } from '@/guards/user.guard'; 19 | 20 | @ApiTags('Doações') 21 | @Controller('donation/order') 22 | export class DonationOrderController { 23 | private logger = new Logger(DonationOrderController.name); 24 | 25 | constructor(private readonly donationOrderService: DonationOrderService) {} 26 | 27 | @Post('') 28 | @UseGuards(UserGuard) 29 | async store(@Body() body, @Request() req) { 30 | try { 31 | const { userId } = req.user; 32 | const data = await this.donationOrderService.store({ ...body, userId }); 33 | return new ServerResponse(200, 'Successfully store donation order', data); 34 | } catch (err: any) { 35 | this.logger.error(`Failed to store donation order: ${err}`); 36 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 37 | } 38 | } 39 | 40 | @Put(':orderId') 41 | @UseGuards(UserGuard) 42 | async update( 43 | @Request() req, 44 | @Param('orderId') orderId: string, 45 | @Body() body, 46 | ) { 47 | try { 48 | const { userId } = req.user; 49 | await this.donationOrderService.update(orderId, userId, body); 50 | return new ServerResponse(200, 'Successfully updated donation order'); 51 | } catch (err: any) { 52 | this.logger.error(`Failed to update donation order: ${err}`); 53 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 54 | } 55 | } 56 | 57 | @Get('') 58 | @UseGuards(UserGuard) 59 | async index(@Query() query, @Request() req) { 60 | try { 61 | const { userId } = req.user; 62 | const data = await this.donationOrderService.index(userId, query); 63 | return new ServerResponse( 64 | 200, 65 | 'Successfully get all donation orders', 66 | data, 67 | ); 68 | } catch (err: any) { 69 | this.logger.error(`Failed to get all donation orders: ${err}`); 70 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 71 | } 72 | } 73 | 74 | @Get(':id') 75 | @UseGuards(UserGuard) 76 | async show(@Param('id') id: string, @Request() req) { 77 | try { 78 | const { userId } = req.user; 79 | const data = await this.donationOrderService.show(id, userId); 80 | if (!data) throw new HttpException('Not founded donation order', 404); 81 | return new ServerResponse(200, 'Successfully get donation order', data); 82 | } catch (err: any) { 83 | this.logger.error(`Failed to get donation order: ${err}`); 84 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/donation-order/donation-order.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DonationOrderService } from './donation-order.service'; 4 | import { DonationOrderController } from './donation-order.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [DonationOrderService], 10 | controllers: [DonationOrderController], 11 | }) 12 | export class DonationOrderModule {} 13 | -------------------------------------------------------------------------------- /src/donation-order/donation-order.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { DonationOrderService } from './donation-order.service'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | describe('DonationOrderService', () => { 8 | let service: DonationOrderService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | imports: [PrismaModule], 13 | providers: [DonationOrderService], 14 | }) 15 | .useMocker((token) => { 16 | if (token === PrismaService) { 17 | return {}; 18 | } 19 | }) 20 | .compile(); 21 | 22 | service = module.get(DonationOrderService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/donation-order/donation-order.service.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { HttpException, Injectable } from '@nestjs/common'; 3 | import { DonationOrderStatus, Prisma } from '@prisma/client'; 4 | import { DefaultArgs } from '@prisma/client/runtime/library'; 5 | 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | import { CreateDonationOrderScheme, UpdateDonationOrderScheme } from './types'; 8 | import { SearchSchema } from '../types'; 9 | 10 | @Injectable() 11 | export class DonationOrderService { 12 | private donationOrderVisibleFields: Prisma.DonationOrderSelect = { 13 | id: true, 14 | status: true, 15 | user: { 16 | select: { 17 | id: true, 18 | name: true, 19 | lastName: true, 20 | phone: true, 21 | }, 22 | }, 23 | shelter: { 24 | select: { 25 | id: true, 26 | name: true, 27 | address: true, 28 | }, 29 | }, 30 | donationOrderSupplies: { 31 | select: { 32 | quantity: true, 33 | supply: { 34 | select: { 35 | name: true, 36 | measure: true, 37 | }, 38 | }, 39 | }, 40 | }, 41 | createdAt: true, 42 | updatedAt: true, 43 | }; 44 | 45 | constructor(private readonly prismaService: PrismaService) {} 46 | 47 | async index(userId: string, query: any) { 48 | const { shelterId, op } = query; 49 | const { order, orderBy, page, perPage } = SearchSchema.parse(query); 50 | 51 | let where = {}; 52 | 53 | if (op === 'received') { 54 | where = await this.getAllReceivedDonations(userId); 55 | } else { 56 | where = this.getAllDonationsMade(userId, shelterId); 57 | } 58 | 59 | const count = await this.prismaService.donationOrder.count({ where }); 60 | 61 | const take = perPage; 62 | const skip = perPage * (page - 1); 63 | 64 | const whereData: Prisma.DonationOrderFindManyArgs = { 65 | take, 66 | skip, 67 | orderBy: { [orderBy]: order }, 68 | where, 69 | }; 70 | 71 | const results = await this.prismaService.donationOrder.findMany({ 72 | ...whereData, 73 | select: this.donationOrderVisibleFields, 74 | orderBy: { 75 | createdAt: 'desc', 76 | }, 77 | }); 78 | 79 | return { 80 | page, 81 | perPage, 82 | count, 83 | results, 84 | }; 85 | } 86 | 87 | async show(id: string, userId: string) { 88 | const data = await this.prismaService.donationOrder.findUnique({ 89 | where: { id, userId }, 90 | select: this.donationOrderVisibleFields, 91 | }); 92 | return data; 93 | } 94 | 95 | async store(body: z.infer) { 96 | const { supplies, shelterId, userId } = 97 | CreateDonationOrderScheme.parse(body); 98 | const donation = await this.prismaService.donationOrder.create({ 99 | data: { 100 | shelterId, 101 | userId, 102 | createdAt: new Date().toISOString(), 103 | donationOrderSupplies: { 104 | createMany: { 105 | data: supplies.map((s) => ({ 106 | supplyId: s.id, 107 | quantity: s.quantity, 108 | createdAt: new Date().toISOString(), 109 | })), 110 | }, 111 | }, 112 | }, 113 | }); 114 | 115 | return donation; 116 | } 117 | 118 | async update( 119 | orderId: string, 120 | userId: string, 121 | body: z.infer, 122 | ) { 123 | const { status } = UpdateDonationOrderScheme.parse(body); 124 | const order = await this.prismaService.donationOrder.findFirst({ 125 | where: { id: orderId }, 126 | select: { 127 | shelterId: true, 128 | userId: true, 129 | donationOrderSupplies: true, 130 | }, 131 | }); 132 | 133 | if (!order) return new HttpException('Donation not found', 404); 134 | 135 | if (order.userId !== userId) { 136 | const isEmployer = await this.prismaService.shelterUsers.findFirst({ 137 | where: { 138 | userId, 139 | shelterId: order.shelterId, 140 | }, 141 | }); 142 | 143 | if (!isEmployer) 144 | return new HttpException( 145 | 'User not allowed to update this donation', 146 | 404, 147 | ); 148 | } 149 | 150 | const updatePromises = 151 | status === DonationOrderStatus.Complete 152 | ? order.donationOrderSupplies.map((d) => 153 | this.prismaService.shelterSupply.update({ 154 | where: { 155 | shelterId_supplyId: { 156 | shelterId: order.shelterId, 157 | supplyId: d.supplyId, 158 | }, 159 | }, 160 | data: { 161 | quantity: { 162 | decrement: d.quantity, 163 | }, 164 | }, 165 | }), 166 | ) 167 | : []; 168 | 169 | await this.prismaService.$transaction([ 170 | ...updatePromises, 171 | this.prismaService.donationOrder.update({ 172 | where: { 173 | id: orderId, 174 | }, 175 | data: { 176 | status, 177 | updatedAt: new Date().toISOString(), 178 | }, 179 | }), 180 | ]); 181 | } 182 | 183 | private async getAllReceivedDonations(userId: string, shelterId?: string) { 184 | const where: Prisma.DonationOrderWhereInput = { 185 | shelterId, 186 | }; 187 | 188 | if (!shelterId) { 189 | const sheltersByUser = await this.prismaService.shelterUsers.findMany({ 190 | where: { 191 | userId, 192 | }, 193 | select: { 194 | shelterId: true, 195 | }, 196 | }); 197 | 198 | const shelterIds = sheltersByUser.map((s) => s.shelterId); 199 | where.shelterId = { 200 | in: shelterIds, 201 | }; 202 | } 203 | 204 | return where; 205 | } 206 | 207 | private getAllDonationsMade(userId: string, shelterId?: string) { 208 | const where: Prisma.DonationOrderWhereInput = { 209 | userId, 210 | shelterId, 211 | }; 212 | 213 | return where; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/donation-order/types.ts: -------------------------------------------------------------------------------- 1 | import { DonationOrderStatus } from '@prisma/client'; 2 | import z from 'zod'; 3 | 4 | const DonationOrderScheme = z.object({ 5 | id: z.string(), 6 | userId: z.string(), 7 | shelterId: z.string(), 8 | status: z.enum([ 9 | DonationOrderStatus.Canceled, 10 | DonationOrderStatus.Complete, 11 | DonationOrderStatus.Pending, 12 | ]), 13 | createdAt: z.string(), 14 | updatedAt: z.string().nullish(), 15 | }); 16 | 17 | const CreateDonationOrderScheme = DonationOrderScheme.omit({ 18 | id: true, 19 | status: true, 20 | createdAt: true, 21 | updatedAt: true, 22 | }).extend({ 23 | supplies: z.array( 24 | z.object({ 25 | id: z.string(), 26 | quantity: z.number().min(1), 27 | }), 28 | ), 29 | }); 30 | 31 | const UpdateDonationOrderScheme = DonationOrderScheme.pick({ 32 | status: true, 33 | }); 34 | 35 | export { 36 | DonationOrderScheme, 37 | CreateDonationOrderScheme, 38 | UpdateDonationOrderScheme, 39 | }; 40 | -------------------------------------------------------------------------------- /src/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, HttpException, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AccessLevel } from '@prisma/client'; 4 | 5 | import { canActivate } from './utils'; 6 | import { PrismaService } from 'src/prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class AdminGuard extends AuthGuard('jwt') { 10 | constructor(private readonly prismaService: PrismaService) { 11 | super(); 12 | } 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | await super.canActivate(context); 16 | const ok = await canActivate(this.prismaService, context, [ 17 | AccessLevel.Admin, 18 | ]); 19 | if (ok) return true; 20 | 21 | throw new HttpException('Acesso não autorizado', 401); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/guards/apply-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class ApplyUser extends AuthGuard('jwt') { 6 | handleRequest(err: any, user: any) { 7 | if (user) return user; 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/guards/distribution-center.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, HttpException, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AccessLevel } from '@prisma/client'; 4 | 5 | import { canActivate } from './utils'; 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class DistributionCenterGuard extends AuthGuard('jwt') { 10 | constructor(private readonly prismaService: PrismaService) { 11 | super(); 12 | } 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | await super.canActivate(context); 16 | const ok = await canActivate(this.prismaService, context, [ 17 | AccessLevel.Admin, 18 | AccessLevel.DistributionCenter, 19 | ]); 20 | if (ok) return true; 21 | 22 | throw new HttpException('Acesso não autorizado', 401); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/guards/hmac.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import * as crypto from 'crypto'; 8 | 9 | @Injectable() 10 | export class HmacGuard implements CanActivate { 11 | constructor() {} 12 | 13 | canActivate(context: ExecutionContext): boolean { 14 | const request = context.switchToHttp().getRequest(); 15 | const hmacHeader = request.headers['x-hmac-signature']; 16 | const timestamp = request.headers['x-hmac-timestamp']; 17 | 18 | if (!hmacHeader || !timestamp) { 19 | throw new UnauthorizedException(); 20 | } 21 | 22 | const secretKey = process.env.HMAC_SECRET_KEY ?? ''; 23 | const currentTimestamp = Math.floor(Date.now() / 1000); 24 | 25 | if (Math.abs(currentTimestamp - parseInt(timestamp)) > 10) { 26 | throw new UnauthorizedException(); 27 | } 28 | 29 | const payload = `${request.method}:${request.url}:${timestamp}:${JSON.stringify(request.body)}`; 30 | 31 | const hmac = crypto 32 | .createHmac('sha256', secretKey) 33 | .update(payload) 34 | .digest('hex'); 35 | 36 | if (hmac !== hmacHeader) { 37 | throw new UnauthorizedException(); 38 | } 39 | 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/guards/staff.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, HttpException, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AccessLevel } from '@prisma/client'; 4 | 5 | import { canActivate } from './utils'; 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class StaffGuard extends AuthGuard('jwt') { 10 | constructor(private readonly prismaService: PrismaService) { 11 | super(); 12 | } 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | await super.canActivate(context); 16 | const ok = await canActivate(this.prismaService, context, [ 17 | AccessLevel.Admin, 18 | AccessLevel.Staff, 19 | ]); 20 | if (ok) return true; 21 | 22 | throw new HttpException('Acesso não autorizado', 401); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/guards/user.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, HttpException, Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AccessLevel } from '@prisma/client'; 4 | 5 | import { canActivate } from './utils'; 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class UserGuard extends AuthGuard('jwt') { 10 | constructor(private readonly prismaService: PrismaService) { 11 | super(); 12 | } 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | await super.canActivate(context); 16 | const ok = await canActivate(this.prismaService, context, [ 17 | AccessLevel.User, 18 | AccessLevel.Staff, 19 | AccessLevel.DistributionCenter, 20 | AccessLevel.Admin, 21 | ]); 22 | if (ok) return true; 23 | throw new HttpException('Acesso não autorizado', 401); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/guards/utils.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from '@nestjs/common'; 2 | import { AccessLevel } from '@prisma/client'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | 6 | async function canActivate( 7 | prismaService: PrismaService, 8 | context: ExecutionContext, 9 | allowed: AccessLevel[], 10 | ) { 11 | const http = context.switchToHttp(); 12 | const request = http.getRequest(); 13 | if (request.user) { 14 | const { userId, sessionId } = request.user; 15 | 16 | return isRightSessionRole(prismaService, allowed, sessionId, userId); 17 | } 18 | 19 | return false; 20 | } 21 | 22 | async function isRightSessionRole( 23 | prismaService: PrismaService, 24 | allowed: AccessLevel[], 25 | sessionId?: string, 26 | userId?: string, 27 | ) { 28 | if (!sessionId) return false; 29 | if (!userId) return false; 30 | 31 | const session = await prismaService.session.findUnique({ 32 | where: { id: sessionId, active: true, user: { id: userId } }, 33 | include: { 34 | user: true, 35 | }, 36 | }); 37 | 38 | if ( 39 | session && 40 | allowed.some((permission) => permission === session.user.accessLevel) 41 | ) { 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | export { canActivate }; 48 | -------------------------------------------------------------------------------- /src/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponseInterceptor } from './interceptors'; 2 | 3 | export { ServerResponseInterceptor }; 4 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponseInterceptor } from './server-response'; 2 | import { ShelterSupplyHistoryInterceptor } from './shelter-supply-history'; 3 | 4 | export { ServerResponseInterceptor, ShelterSupplyHistoryInterceptor }; 5 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/server-response/index.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponseInterceptor } from './server-response.interceptor'; 2 | 3 | export { ServerResponseInterceptor }; 4 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/server-response/server-response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import { FastifyReply } from 'fastify'; 10 | 11 | import { ServerResponse } from '@/utils/utils'; 12 | 13 | @Injectable() 14 | export class ServerResponseInterceptor implements NestInterceptor { 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | return next.handle().pipe( 17 | map((result) => { 18 | if (result instanceof ServerResponse) { 19 | const response: FastifyReply = context.switchToHttp().getResponse(); 20 | const data = result.data; 21 | response.status(data.statusCode); 22 | return data; 23 | } else { 24 | return result; 25 | } 26 | }), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/shelter-supply-history/index.ts: -------------------------------------------------------------------------------- 1 | import { ShelterSupplyHistoryInterceptor } from './shelter-supply-history.interceptor'; 2 | 3 | export { ShelterSupplyHistoryInterceptor }; 4 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/shelter-supply-history/shelter-supply-history.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | 9 | import { ShelterSupplyHistoryAction } from './types'; 10 | import { handler } from './utils'; 11 | import { PrismaService } from '../../../prisma/prisma.service'; 12 | 13 | @Injectable() 14 | export class ShelterSupplyHistoryInterceptor implements NestInterceptor { 15 | constructor(private readonly action: ShelterSupplyHistoryAction) {} 16 | 17 | intercept(context: ExecutionContext, next: CallHandler): Observable { 18 | const request = context.switchToHttp().getRequest(); 19 | handler(PrismaService.getInstance(), this.action, request); 20 | return next.handle(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/shelter-supply-history/types.ts: -------------------------------------------------------------------------------- 1 | export enum ShelterSupplyHistoryAction { 2 | Create = 'shelter-supply-history-action/create', 3 | Update = 'shelter-supply-history-action/update', 4 | UpdateMany = 'shelter-supply-history-action/update-many', 5 | } 6 | 7 | export interface UserIdentity { 8 | ip?: string; 9 | userAgent?: string; 10 | userId?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/interceptors/interceptors/shelter-supply-history/utils.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { FastifyRequest } from 'fastify'; 3 | import { Logger } from '@nestjs/common'; 4 | 5 | import { 6 | CreateShelterSupplySchema, 7 | UpdateShelterSupplySchema, 8 | } from '../../../shelter-supply/types'; 9 | import { PrismaService } from '../../../prisma/prisma.service'; 10 | import { CreateSupplyHistorySchema } from '../../../supplies-history/types'; 11 | import { ShelterSupplyHistoryAction, UserIdentity } from './types'; 12 | import { getSessionData } from '@/utils'; 13 | 14 | function registerSupplyLog( 15 | body: z.infer, 16 | user: UserIdentity = {}, 17 | ) { 18 | const fn = async () => { 19 | const { shelterId, supplyId, ...rest } = 20 | CreateSupplyHistorySchema.parse(body); 21 | 22 | const prev = await PrismaService.getInstance().supplyHistory.findFirst({ 23 | where: { 24 | shelterId, 25 | supplyId, 26 | }, 27 | orderBy: { 28 | createdAt: 'desc', 29 | }, 30 | }); 31 | 32 | await PrismaService.getInstance().supplyHistory.create({ 33 | data: { 34 | shelterId, 35 | supplyId, 36 | ...rest, 37 | ...user, 38 | createdAt: new Date().toISOString(), 39 | predecessorId: prev?.id, 40 | }, 41 | }); 42 | }; 43 | 44 | fn() 45 | .then(() => { 46 | Logger.log('Successfully saved shelter supply log', 'ShelterSupplyLog'); 47 | }) 48 | .catch((err) => { 49 | Logger.error( 50 | `Failed to save shelter supply log: ${err}`, 51 | 'ShelterSupplyLog', 52 | ); 53 | }); 54 | } 55 | 56 | function registerCreateSupplyLog( 57 | body: z.infer, 58 | user: UserIdentity, 59 | ) { 60 | const payload = CreateShelterSupplySchema.parse(body); 61 | registerSupplyLog(payload, user); 62 | } 63 | 64 | function registerUpdateSupplyLog( 65 | body: z.infer, 66 | user: UserIdentity, 67 | ) { 68 | const payload = UpdateShelterSupplySchema.parse(body); 69 | 70 | registerSupplyLog( 71 | { 72 | shelterId: payload.where.shelterId, 73 | supplyId: payload.where.supplyId, 74 | priority: payload.data.priority, 75 | quantity: payload.data.quantity, 76 | }, 77 | user, 78 | ); 79 | } 80 | 81 | function handler( 82 | prismaService: PrismaService, 83 | action: ShelterSupplyHistoryAction, 84 | request: FastifyRequest, 85 | ) { 86 | const headers = request.headers; 87 | const token = headers['authorization']; 88 | const user: UserIdentity = { 89 | ip: headers['x-real-ip']?.toString(), 90 | userAgent: headers['user-agent'], 91 | }; 92 | 93 | if (token) { 94 | const { userId } = getSessionData(token); 95 | user.userId = userId; 96 | } 97 | 98 | switch (action) { 99 | case ShelterSupplyHistoryAction.Create: 100 | registerCreateSupplyLog(request.body as any, user); 101 | break; 102 | case ShelterSupplyHistoryAction.Update: 103 | registerUpdateSupplyLog( 104 | { 105 | data: request.body as any, 106 | where: request.params as any, 107 | }, 108 | user, 109 | ); 110 | break; 111 | } 112 | } 113 | 114 | export { handler, registerSupplyLog }; 115 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 7 | 8 | import { AppModule } from './app.module'; 9 | 10 | async function bootstrap() { 11 | const fastifyAdapter = new FastifyAdapter(); 12 | const app = await NestFactory.create( 13 | AppModule, 14 | fastifyAdapter, 15 | ); 16 | 17 | const config = new DocumentBuilder() 18 | .setTitle('SOS - Rio Grande do Sul') 19 | .setDescription('...') 20 | .setVersion('1.0') 21 | .addBearerAuth() 22 | .build(); 23 | const document = SwaggerModule.createDocument(app, config); 24 | SwaggerModule.setup('api', app, document); 25 | 26 | const HOST = process.env.HOST || '127.0.0.1'; 27 | const PORT = Number(process.env.PORT) || 3000; 28 | app.enableCors({ origin: [/^(.*)/] }); 29 | 30 | await app.listen(PORT, HOST, () => { 31 | console.log(`Listening on: ${HOST}:${PORT}`); 32 | }); 33 | } 34 | 35 | bootstrap(); 36 | -------------------------------------------------------------------------------- /src/middlewares/logging.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; 2 | import { FastifyReply, FastifyRequest } from 'fastify'; 3 | 4 | import { getSessionData } from '@/utils'; 5 | 6 | @Injectable() 7 | export class LoggingMiddleware implements NestMiddleware { 8 | private logger = new Logger(LoggingMiddleware.name); 9 | use(req: FastifyRequest, _: FastifyReply, next: () => void) { 10 | const { method, originalUrl, headers } = req; 11 | const ip = headers['x-real-ip'] || req.ip; 12 | const token = headers.authorization?.split('Bearer ').at(-1); 13 | const appVersion = headers['app-version']; 14 | const userAgent = headers['user-agent']; 15 | if (!headers['content-type']) headers['content-type'] = 'application/json'; 16 | const { userId } = getSessionData(token); 17 | const message = `${appVersion} - ${method} ${originalUrl} ${ip} ${userAgent} (${userId})`; 18 | this.logger.log(message); 19 | 20 | next(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/partners/partners.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PartnersController } from './partners.controller'; 3 | import { PartnersService } from './partners.service'; 4 | import { ServerResponse } from '../utils'; 5 | 6 | describe('PartnersController', () => { 7 | let controller: PartnersController; 8 | let service: PartnersService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | controllers: [PartnersController], 13 | providers: [PartnersService], 14 | }) 15 | .overrideProvider(PartnersService) 16 | .useValue({ 17 | index: jest.fn().mockResolvedValue([ 18 | { 19 | id: 1, 20 | name: 'Partner 1', 21 | link: 'https://partner1.com', 22 | }, 23 | ]), 24 | store: jest.fn().mockResolvedValue({}), 25 | }) 26 | .compile(); 27 | 28 | controller = module.get(PartnersController); 29 | service = module.get(PartnersService); 30 | }); 31 | 32 | it('should return all partners', async () => { 33 | const expectedResponse = new ServerResponse( 34 | 200, 35 | 'Successfully get partners', 36 | [ 37 | { 38 | id: 1, 39 | name: 'Partner 1', 40 | link: 'https://partner1.com', 41 | }, 42 | ], 43 | ); 44 | const result = await controller.index(); 45 | expect(result).toEqual(expectedResponse); 46 | }); 47 | 48 | it('should create a partner', async () => { 49 | const expectedResponse = new ServerResponse( 50 | 201, 51 | 'Successfully created partner', 52 | ); 53 | const result = await controller.store({ 54 | name: 'Partner 1', 55 | link: 'https://partner1.com', 56 | }); 57 | expect(result).toEqual(expectedResponse); 58 | }); 59 | 60 | it('should throw an error when store fails', async () => { 61 | const errorMessage = 'Failed to create partner'; 62 | jest.spyOn(service, 'store').mockRejectedValue(new Error(errorMessage)); 63 | await expect( 64 | controller.store({ 65 | name: 'Partner 1', 66 | link: 'https://partner1.com', 67 | }), 68 | ).rejects.toThrow(); 69 | }); 70 | 71 | it('should throw an error when index fails', async () => { 72 | const errorMessage = 'Failed to get partners'; 73 | jest.spyOn(service, 'index').mockRejectedValue(new Error(errorMessage)); 74 | await expect(controller.index()).rejects.toThrow(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/partners/partners.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Post, 8 | UseGuards, 9 | } from '@nestjs/common'; 10 | import { PartnersService } from './partners.service'; 11 | import { ServerResponse } from '../utils'; 12 | import { ApiTags } from '@nestjs/swagger'; 13 | import { AdminGuard } from '@/guards/admin.guard'; 14 | 15 | @ApiTags('Parceiros') 16 | @Controller('partners') 17 | export class PartnersController { 18 | private logger = new Logger(PartnersController.name); 19 | 20 | constructor(private readonly partnersService: PartnersService) {} 21 | 22 | @Get('') 23 | async index() { 24 | try { 25 | const data = await this.partnersService.index(); 26 | return new ServerResponse(200, 'Successfully get partners', data); 27 | } catch (err: any) { 28 | this.logger.error(`Failed to get partners: ${err}`); 29 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 30 | } 31 | } 32 | 33 | @Post('') 34 | @UseGuards(AdminGuard) 35 | async store(@Body() body) { 36 | try { 37 | await this.partnersService.store(body); 38 | return new ServerResponse(201, 'Successfully created partner'); 39 | } catch (err: any) { 40 | this.logger.error(`Failed to create partner: ${err}`); 41 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/partners/partners.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PartnersService } from './partners.service'; 4 | import { PartnersController } from './partners.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [PartnersService], 10 | controllers: [PartnersController], 11 | }) 12 | export class PartnersModule {} 13 | -------------------------------------------------------------------------------- /src/partners/partners.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PartnersService } from './partners.service'; 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | 5 | jest.mock('../prisma/prisma.service'); 6 | 7 | describe('PartnersService', () => { 8 | let service: PartnersService; 9 | let prismaService: PrismaService; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [ 14 | PartnersService, 15 | { provide: PrismaService, useClass: PrismaService }, 16 | ], 17 | }) 18 | .overrideProvider(PrismaService) 19 | .useValue({ 20 | partners: { 21 | findMany: jest.fn().mockResolvedValue([]), 22 | create: jest.fn().mockResolvedValue({}), 23 | }, 24 | }) 25 | .compile(); 26 | 27 | service = module.get(PartnersService); 28 | prismaService = module.get(PrismaService); 29 | }); 30 | it('should return all partners', async () => { 31 | const partners = await service.index(); 32 | expect(partners).toEqual([]); 33 | }); 34 | 35 | it('should create a partner', async () => { 36 | await service.store({ 37 | name: 'Partner 1', 38 | link: 'https://partner1.com', 39 | }); 40 | 41 | expect(prismaService.partners.create).toHaveBeenCalledWith({ 42 | data: { 43 | name: 'Partner 1', 44 | link: 'https://partner1.com', 45 | createdAt: new Date().toISOString(), 46 | }, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/partners/partners.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { z } from 'zod'; 6 | import { CreatePartnerSchema } from './types'; 7 | 8 | @ApiTags('Parceiros') 9 | @Injectable() 10 | export class PartnersService { 11 | constructor(private readonly prismaService: PrismaService) {} 12 | 13 | async index() { 14 | return await this.prismaService.partners.findMany({}); 15 | } 16 | 17 | async store(body: z.infer) { 18 | const payload = CreatePartnerSchema.parse(body); 19 | await this.prismaService.partners.create({ 20 | data: { ...payload, createdAt: new Date().toISOString() }, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/partners/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | const PartnerSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | link: z.string(), 7 | createdAt: z.string(), 8 | updatedAt: z.string().nullable().optional(), 9 | }); 10 | 11 | const CreatePartnerSchema = PartnerSchema.omit({ 12 | id: true, 13 | createdAt: true, 14 | updatedAt: true, 15 | }); 16 | 17 | export { PartnerSchema, CreatePartnerSchema }; 18 | -------------------------------------------------------------------------------- /src/prisma/hooks/user/index.ts: -------------------------------------------------------------------------------- 1 | import { hooks } from './user-hooks'; 2 | 3 | export { hooks }; 4 | -------------------------------------------------------------------------------- /src/prisma/hooks/user/user-hooks.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@prisma/client'; 2 | import * as bcrypt from 'bcrypt'; 3 | 4 | function cryptPassword(params: Prisma.MiddlewareParams) { 5 | if ( 6 | (params.action === 'create' || params.action === 'update') && 7 | params.model === 'User' && 8 | !!params.args.data.password 9 | ) { 10 | const user = params.args.data; 11 | const salt = bcrypt.genSaltSync(10); 12 | const hash = bcrypt.hashSync(user.password, salt); 13 | user.password = hash; 14 | params.args.data = user; 15 | } 16 | } 17 | 18 | const hooks = [cryptPassword]; 19 | 20 | export { hooks }; 21 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaService } from './prisma.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [PrismaService], 8 | exports: [PrismaService], 9 | }) 10 | export class PrismaModule {} 11 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | describe('PrismaService', () => { 5 | let service: PrismaService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [PrismaService], 10 | }).compile(); 11 | 12 | service = module.get(PrismaService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { Prisma, PrismaClient } from '@prisma/client'; 3 | 4 | import { hooks as userHooks } from './hooks/user'; 5 | 6 | @Injectable() 7 | export class PrismaService 8 | extends PrismaClient< 9 | Prisma.PrismaClientOptions, 10 | 'query' | 'info' | 'warn' | 'error' | 'beforeExit' 11 | > 12 | implements OnModuleInit 13 | { 14 | private static instance: PrismaService; 15 | 16 | constructor() { 17 | super(); 18 | } 19 | 20 | static getInstance(): PrismaService { 21 | if (!PrismaService.instance) { 22 | PrismaService.instance = new PrismaService(); 23 | PrismaService.instance.$connect(); 24 | } 25 | return PrismaService.instance; 26 | } 27 | 28 | async onModuleInit() { 29 | await this.$connect(); 30 | this.$use((params, next) => { 31 | userHooks.forEach((fn) => fn(params)); 32 | return next(params); 33 | }); 34 | } 35 | 36 | async enableShutdownHooks(app: INestApplication) { 37 | this.$on('beforeExit', async () => { 38 | await app.close(); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sessions/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | 5 | import { TokenPayload } from './types'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor() { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: true, 13 | secretOrKey: process.env.SECRET_KEY || 'batata', 14 | }); 15 | } 16 | 17 | async validate(payload: TokenPayload) { 18 | return { ...payload }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sessions/sessions.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from '@nestjs/jwt'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { SessionsController } from './sessions.controller'; 5 | import { SessionsService } from './sessions.service'; 6 | 7 | describe('SessionsController', () => { 8 | let controller: SessionsController; 9 | let service: SessionsService; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | controllers: [SessionsController], 14 | providers: [SessionsService, JwtService], 15 | }) 16 | .useMocker((token) => { 17 | if (token === PrismaService) { 18 | return {}; 19 | } 20 | }) 21 | .overrideProvider(SessionsService) 22 | .useValue({ 23 | login: jest.fn().mockResolvedValue('token'), 24 | show: jest.fn().mockResolvedValue('token'), 25 | delete: jest.fn().mockResolvedValue('token'), 26 | }) 27 | .compile(); 28 | 29 | module.useLogger({ 30 | log: jest.fn(), 31 | error: jest.fn(), 32 | warn: jest.fn(), 33 | verbose: jest.fn(), 34 | }); 35 | controller = module.get(SessionsController); 36 | service = module.get(SessionsService); 37 | }); 38 | 39 | it('should return a token', async () => { 40 | const expectedResponse = { 41 | statusCode: 200, 42 | message: 'Successfully logged in', 43 | respData: 'token', 44 | }; 45 | const res = await controller.login( 46 | { 47 | email: 'test@test.com', 48 | password: 'password', 49 | }, 50 | '127.0.0.1', 51 | 'user-agent', 52 | ); 53 | expect(res).toEqual(expectedResponse); 54 | }); 55 | 56 | it('should throw an error when login', async () => { 57 | jest.spyOn(service, 'login').mockRejectedValue(new Error('Error')); 58 | await expect( 59 | controller.login( 60 | { 61 | email: 'test@test.com', 62 | password: 'password', 63 | }, 64 | '127.0.0.1', 65 | 'user-agent', 66 | ), 67 | ).rejects.toThrow(); 68 | }); 69 | 70 | it('should show a session', async () => { 71 | const expectedResponse = { 72 | statusCode: 200, 73 | message: 'Successfully logged in', 74 | respData: 'token', 75 | }; 76 | const res = await controller.show({ user: { userId: 1 } }); 77 | expect(res).toEqual(expectedResponse); 78 | }); 79 | 80 | it('should throw an error when show an user', async () => { 81 | jest.spyOn(service, 'show').mockRejectedValue(new Error('Error')); 82 | await expect(controller.show({ user: { userId: 1 } })).rejects.toThrow(); 83 | }); 84 | 85 | it('should delete a session', async () => { 86 | const expectedResponse = { 87 | statusCode: 200, 88 | message: 'Successfully logged out', 89 | respData: 'token', 90 | }; 91 | 92 | const res = await controller.delete({ user: { userId: 1 } }); 93 | expect(res).toEqual(expectedResponse); 94 | }); 95 | 96 | it('should throw an error when delete a session', async () => { 97 | jest.spyOn(service, 'delete').mockRejectedValue(new Error('Error')); 98 | await expect(controller.delete({ user: { userId: 1 } })).rejects.toThrow(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/sessions/sessions.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Headers, 7 | HttpException, 8 | Logger, 9 | Post, 10 | Request, 11 | UseGuards, 12 | } from '@nestjs/common'; 13 | import { ApiTags } from '@nestjs/swagger'; 14 | 15 | import { UserGuard } from '@/guards/user.guard'; 16 | import { ServerResponse } from '../utils'; 17 | import { SessionsService } from './sessions.service'; 18 | 19 | @ApiTags('Sessões') 20 | @Controller('sessions') 21 | export class SessionsController { 22 | private logger = new Logger(SessionsController.name); 23 | 24 | constructor(private readonly sessionService: SessionsService) {} 25 | 26 | @Post('') 27 | async login( 28 | @Body() body, 29 | @Headers('x-real-ip') ip: string, 30 | @Headers('user-agent') userAgent: string, 31 | ) { 32 | try { 33 | const data = await this.sessionService.login({ ...body, ip, userAgent }); 34 | return new ServerResponse(200, 'Successfully logged in', data); 35 | } catch (err: any) { 36 | this.logger.error(`Failed to login ${err}`); 37 | throw new HttpException( 38 | err?.message ?? err?.code ?? err?.name ?? `${err}`, 39 | 400, 40 | ); 41 | } 42 | } 43 | 44 | @Get('') 45 | @UseGuards(UserGuard) 46 | async show(@Request() req) { 47 | try { 48 | const { userId } = req.user; 49 | const data = await this.sessionService.show(userId); 50 | return new ServerResponse(200, 'Successfully logged in', data); 51 | } catch (err: any) { 52 | this.logger.error(`Failed to login ${err}`); 53 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 54 | } 55 | } 56 | 57 | @Delete('') 58 | @UseGuards(UserGuard) 59 | async delete(@Request() req) { 60 | try { 61 | const data = await this.sessionService.delete(req.user); 62 | return new ServerResponse(200, 'Successfully logged out', data); 63 | } catch (err: any) { 64 | this.logger.error(`Failed to logged out ${err}`); 65 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/sessions/sessions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | 4 | import { PrismaModule } from '../prisma/prisma.module'; 5 | import { JwtStrategy } from './jwt.strategy'; 6 | import { SessionsController } from './sessions.controller'; 7 | import { SessionsService } from './sessions.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | PrismaModule, 12 | JwtModule.register({ 13 | secret: process.env.SECRET_KEY || 'batata', 14 | }), 15 | ], 16 | providers: [JwtStrategy, SessionsService], 17 | controllers: [SessionsController], 18 | }) 19 | export class SessionsModule {} 20 | -------------------------------------------------------------------------------- /src/sessions/sessions.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import * as bcrypt from 'bcrypt'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { SessionsService } from './sessions.service'; 6 | 7 | jest.mock('bcrypt'); 8 | describe('SessionsService', () => { 9 | let service: SessionsService; 10 | let prismaService: PrismaService; 11 | let jwtService: JwtService; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | providers: [ 16 | SessionsService, 17 | { 18 | provide: PrismaService, 19 | useValue: { 20 | user: { 21 | findUnique: jest.fn(), 22 | }, 23 | session: { 24 | updateMany: jest.fn(), 25 | create: jest.fn(), 26 | update: jest.fn(), 27 | }, 28 | }, 29 | }, 30 | { 31 | provide: JwtService, 32 | useValue: { 33 | sign: jest.fn(), 34 | }, 35 | }, 36 | ], 37 | }).compile(); 38 | 39 | service = module.get(SessionsService); 40 | prismaService = module.get(PrismaService); 41 | jwtService = module.get(JwtService); 42 | }); 43 | 44 | describe('login', () => { 45 | it('should throw an error if user is not found', async () => { 46 | (prismaService.user.findUnique as jest.Mock).mockResolvedValue(null); 47 | 48 | await expect( 49 | service.login({ 50 | login: 'test', 51 | password: 'password', 52 | ip: '127.0.0.1', 53 | userAgent: 'test', 54 | }), 55 | ).rejects.toThrow('Usuário não encontrado'); 56 | }); 57 | 58 | it('should throw an error if password is incorrect', async () => { 59 | const user = { id: 1, login: 'test', password: 'hashedpassword' }; 60 | (prismaService.user.findUnique as jest.Mock).mockResolvedValue(user); 61 | (bcrypt.compare as jest.Mock).mockResolvedValue(false); 62 | 63 | await expect( 64 | service.login({ 65 | login: 'test', 66 | password: 'wrongpassword', 67 | ip: '127.0.0.1', 68 | userAgent: 'test', 69 | }), 70 | ).rejects.toThrow('Senha incorreta'); 71 | }); 72 | 73 | it('should return a token if login is successful', async () => { 74 | const user = { id: 1, login: 'test', password: 'hashedpassword' }; 75 | const session = { id: 1, userId: 1 }; 76 | const token = 'token'; 77 | (prismaService.user.findUnique as jest.Mock).mockResolvedValue(user); 78 | (bcrypt.compare as jest.Mock).mockResolvedValue(true); 79 | (prismaService.session.updateMany as jest.Mock).mockResolvedValue({}); 80 | (prismaService.session.create as jest.Mock).mockResolvedValue(session); 81 | (jwtService.sign as jest.Mock).mockReturnValue(token); 82 | 83 | const result = await service.login({ 84 | login: 'test', 85 | password: 'password', 86 | ip: '127.0.0.1', 87 | userAgent: 'test', 88 | }); 89 | 90 | expect(result).toEqual({ token }); 91 | expect(prismaService.user.findUnique).toHaveBeenCalledWith({ 92 | where: { login: 'test' }, 93 | }); 94 | expect(bcrypt.compare).toHaveBeenCalledWith('password', 'hashedpassword'); 95 | expect(prismaService.session.updateMany).toHaveBeenCalledWith({ 96 | where: { user: { login: 'test' }, active: true }, 97 | data: { active: false, updatedAt: expect.any(String) }, 98 | }); 99 | expect(prismaService.session.create).toHaveBeenCalledWith({ 100 | data: { 101 | userId: user.id, 102 | ip: '127.0.0.1', 103 | userAgent: 'test', 104 | createdAt: expect.any(String), 105 | }, 106 | }); 107 | expect(jwtService.sign).toHaveBeenCalledWith({ 108 | sessionId: session.id, 109 | userId: user.id, 110 | }); 111 | }); 112 | }); 113 | 114 | describe('show', () => { 115 | it('should return user data if user is found', async () => { 116 | const user = { 117 | id: '1', 118 | name: 'Test User', 119 | login: 'test', 120 | phone: '123456789', 121 | accessLevel: 'admin', 122 | createdAt: new Date(), 123 | }; 124 | (prismaService.user.findUnique as jest.Mock).mockResolvedValue(user); 125 | 126 | const result = await service.show('1'); 127 | 128 | expect(result).toEqual(user); 129 | expect(prismaService.user.findUnique).toHaveBeenCalledWith({ 130 | where: { id: '1' }, 131 | select: { 132 | id: true, 133 | name: true, 134 | login: true, 135 | phone: true, 136 | accessLevel: true, 137 | createdAt: true, 138 | }, 139 | }); 140 | }); 141 | 142 | it('should return null if user is not found', async () => { 143 | (prismaService.user.findUnique as jest.Mock).mockResolvedValue(null); 144 | 145 | const result = await service.show('1'); 146 | 147 | expect(result).toBeNull(); 148 | expect(prismaService.user.findUnique).toHaveBeenCalledWith({ 149 | where: { id: '1' }, 150 | select: { 151 | id: true, 152 | name: true, 153 | login: true, 154 | phone: true, 155 | accessLevel: true, 156 | createdAt: true, 157 | }, 158 | }); 159 | }); 160 | }); 161 | 162 | describe('delete', () => { 163 | it('should deactivate session if session and user match', async () => { 164 | const session = { 165 | id: '1', 166 | userId: '1', 167 | updatedAt: new Date(), 168 | active: false, 169 | }; 170 | (prismaService.session.update as jest.Mock).mockResolvedValue(session); 171 | 172 | const result = await service.delete({ sessionId: '1', userId: '1' }); 173 | 174 | expect(result).toEqual(session); 175 | expect(prismaService.session.update).toHaveBeenCalledWith({ 176 | where: { 177 | id: '1', 178 | userId: '1', 179 | }, 180 | data: { 181 | updatedAt: expect.any(String), 182 | active: false, 183 | }, 184 | }); 185 | }); 186 | 187 | it('should return null if session does not exist', async () => { 188 | (prismaService.session.update as jest.Mock).mockResolvedValue(null); 189 | 190 | const result = await service.delete({ sessionId: '1', userId: '1' }); 191 | 192 | expect(result).toBeNull(); 193 | expect(prismaService.session.update).toHaveBeenCalledWith({ 194 | where: { 195 | id: '1', 196 | userId: '1', 197 | }, 198 | data: { 199 | updatedAt: expect.any(String), 200 | active: false, 201 | }, 202 | }); 203 | }); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /src/sessions/sessions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import * as bcrypt from 'bcrypt'; 4 | 5 | import { PrismaService } from '../prisma/prisma.service'; 6 | import { LoginSchema, TokenPayload } from './types'; 7 | 8 | @Injectable() 9 | export class SessionsService { 10 | constructor( 11 | private readonly prismaService: PrismaService, 12 | private readonly jwtService: JwtService, 13 | ) {} 14 | 15 | async login(body: any) { 16 | const { login, password, ip, userAgent } = LoginSchema.parse(body); 17 | const user = await this.prismaService.user.findUnique({ 18 | where: { login }, 19 | }); 20 | if (!user) throw new Error('Usuário não encontrado'); 21 | const passwordMatched = await bcrypt.compare(password, user.password); 22 | if (!passwordMatched) throw new Error('Senha incorreta'); 23 | 24 | await this.prismaService.session.updateMany({ 25 | where: { 26 | user: { 27 | login, 28 | }, 29 | active: true, 30 | }, 31 | data: { 32 | active: false, 33 | updatedAt: new Date().toISOString(), 34 | }, 35 | }); 36 | 37 | const session = await this.prismaService.session.create({ 38 | data: { 39 | userId: user.id, 40 | ip, 41 | userAgent, 42 | createdAt: new Date().toISOString(), 43 | }, 44 | }); 45 | 46 | const payload: TokenPayload = { 47 | sessionId: session.id, 48 | userId: user.id, 49 | }; 50 | 51 | return { token: this.jwtService.sign(payload) }; 52 | } 53 | 54 | async show(id: string) { 55 | const data = await this.prismaService.user.findUnique({ 56 | where: { 57 | id, 58 | }, 59 | select: { 60 | id: true, 61 | name: true, 62 | login: true, 63 | phone: true, 64 | accessLevel: true, 65 | createdAt: true, 66 | }, 67 | }); 68 | return data; 69 | } 70 | 71 | async delete(props: TokenPayload) { 72 | const { sessionId, userId } = props; 73 | const data = await this.prismaService.session.update({ 74 | where: { 75 | id: sessionId, 76 | userId, 77 | }, 78 | data: { 79 | updatedAt: new Date().toISOString(), 80 | active: false, 81 | }, 82 | }); 83 | return data; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/sessions/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | const LoginSchema = z.object({ 4 | login: z.string().transform((v) => v.toLowerCase()), 5 | password: z.string(), 6 | ip: z.string().nullable().optional(), 7 | userAgent: z.string().nullable().optional(), 8 | }); 9 | 10 | interface TokenPayload { 11 | sessionId: string; 12 | userId: string; 13 | } 14 | 15 | export { LoginSchema, TokenPayload }; 16 | -------------------------------------------------------------------------------- /src/shelter-managers/shelter-managers.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ShelterManagersController } from './shelter-managers.controller'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { ShelterManagersService } from './shelter-managers.service'; 5 | 6 | describe('ShelterManagersController', () => { 7 | let controller: ShelterManagersController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [ShelterManagersController], 12 | providers: [ShelterManagersService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return {}; 17 | } 18 | }) 19 | .compile(); 20 | 21 | controller = module.get( 22 | ShelterManagersController, 23 | ); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/shelter-managers/shelter-managers.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Query, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { ApiTags } from '@nestjs/swagger'; 13 | 14 | import { ShelterManagersService } from './shelter-managers.service'; 15 | import { ServerResponse } from '../utils'; 16 | import { AdminGuard } from '@/guards/admin.guard'; 17 | 18 | @ApiTags('Admin de Abrigo') 19 | @Controller('shelter/managers') 20 | export class ShelterManagersController { 21 | private logger = new Logger(ShelterManagersController.name); 22 | 23 | constructor( 24 | private readonly shelterManagerServices: ShelterManagersService, 25 | ) {} 26 | 27 | @Post('') 28 | @UseGuards(AdminGuard) 29 | async store(@Body() body) { 30 | try { 31 | await this.shelterManagerServices.store(body); 32 | return new ServerResponse(200, 'Successfully added manager to shelter'); 33 | } catch (err: any) { 34 | this.logger.error(`Failed to added manager to shelter: ${err}`); 35 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 36 | } 37 | } 38 | 39 | @Get(':shelterId') 40 | async index( 41 | @Param('shelterId') shelterId: string, 42 | @Query('includes') includes: string, 43 | ) { 44 | try { 45 | const data = await this.shelterManagerServices.index(shelterId, includes); 46 | return new ServerResponse(200, 'Successfully get shelter managers', data); 47 | } catch (err: any) { 48 | this.logger.error(`Failed to get shelter managers: ${err}`); 49 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/shelter-managers/shelter-managers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ShelterManagersService } from './shelter-managers.service'; 4 | import { ShelterManagersController } from './shelter-managers.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [ShelterManagersService], 10 | controllers: [ShelterManagersController], 11 | }) 12 | export class ShelterManagersModule {} 13 | -------------------------------------------------------------------------------- /src/shelter-managers/shelter-managers.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ShelterManagersService } from './shelter-managers.service'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | describe('ShelterManagersService', () => { 6 | let service: ShelterManagersService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ShelterManagersService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(ShelterManagersService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/shelter-managers/shelter-managers.service.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { CreateShelterManagerSchema } from './types'; 6 | 7 | @Injectable() 8 | export class ShelterManagersService { 9 | constructor(private readonly prismaService: PrismaService) {} 10 | 11 | async store(body: z.infer) { 12 | const { shelterId, userId } = CreateShelterManagerSchema.parse(body); 13 | await this.prismaService.shelterManagers.create({ 14 | data: { 15 | shelterId, 16 | userId, 17 | createdAt: new Date().toISOString(), 18 | }, 19 | }); 20 | } 21 | 22 | async index(shelterId: string, includes: string = 'user') { 23 | const includeData = { 24 | user: { 25 | select: { 26 | id: true, 27 | name: true, 28 | lastName: true, 29 | phone: true, 30 | createdAt: true, 31 | updatedAt: true, 32 | }, 33 | }, 34 | shelter: { 35 | select: { 36 | id: true, 37 | name: true, 38 | contact: true, 39 | address: true, 40 | createdAt: true, 41 | updatedAt: true, 42 | }, 43 | }, 44 | }; 45 | 46 | const selectData = includes 47 | .split(',') 48 | .reduce( 49 | (prev, curr) => 50 | includeData[curr] ? { ...prev, [curr]: includeData[curr] } : prev, 51 | {}, 52 | ); 53 | 54 | const data = await this.prismaService.shelterManagers.findMany({ 55 | where: { 56 | shelterId, 57 | }, 58 | select: { ...selectData, updatedAt: true, createdAt: true }, 59 | }); 60 | return data; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/shelter-managers/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | const ShelterManagerSchema = z.object({ 4 | shelterId: z.string(), 5 | userId: z.string(), 6 | createdAt: z.string(), 7 | updatedAt: z.string().optional().nullable(), 8 | }); 9 | 10 | const CreateShelterManagerSchema = ShelterManagerSchema.pick({ 11 | shelterId: true, 12 | userId: true, 13 | }); 14 | 15 | export { ShelterManagerSchema, CreateShelterManagerSchema }; 16 | -------------------------------------------------------------------------------- /src/shelter-supply/shelter-supply.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ShelterSupplyController } from './shelter-supply.controller'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { ShelterSupplyService } from './shelter-supply.service'; 5 | 6 | describe('ShelterSupplyController', () => { 7 | let controller: ShelterSupplyController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [ShelterSupplyController], 12 | providers: [ShelterSupplyService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return {}; 17 | } 18 | }) 19 | .compile(); 20 | 21 | controller = module.get(ShelterSupplyController); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(controller).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/shelter-supply/shelter-supply.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | } from '@nestjs/common'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | 13 | import { ShelterSupplyService } from './shelter-supply.service'; 14 | import { ServerResponse } from '../utils'; 15 | import { RegisterShelterSupplyHistory } from '@/decorators/RegisterShelterSupplyHistory'; 16 | import { ShelterSupplyHistoryAction } from '@/interceptors/interceptors/shelter-supply-history/types'; 17 | 18 | @ApiTags('Suprimento de abrigos') 19 | @Controller('shelter/supplies') 20 | export class ShelterSupplyController { 21 | private logger = new Logger(ShelterSupplyController.name); 22 | 23 | constructor(private readonly shelterSupplyService: ShelterSupplyService) {} 24 | 25 | @Get(':shelterId') 26 | async index(@Param('shelterId') shelterId: string) { 27 | try { 28 | const data = await this.shelterSupplyService.index(shelterId); 29 | return new ServerResponse(200, 'Successfully get shelter supplies', data); 30 | } catch (err: any) { 31 | this.logger.error(`Failed to get shelter supplies: ${err}`); 32 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 33 | } 34 | } 35 | 36 | @Post('') 37 | @RegisterShelterSupplyHistory(ShelterSupplyHistoryAction.Create) 38 | async store(@Body() body) { 39 | try { 40 | const data = await this.shelterSupplyService.store(body); 41 | return new ServerResponse( 42 | 200, 43 | 'Successfully created shelter supply', 44 | data, 45 | ); 46 | } catch (err: any) { 47 | this.logger.error(`Failed to create shelter supply: ${err}`); 48 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 49 | } 50 | } 51 | 52 | @Put(':shelterId/:supplyId') 53 | @RegisterShelterSupplyHistory(ShelterSupplyHistoryAction.Update) 54 | async update( 55 | @Body() body, 56 | @Param('shelterId') shelterId: string, 57 | @Param('supplyId') supplyId: string, 58 | ) { 59 | try { 60 | const data = await this.shelterSupplyService.update({ 61 | where: { shelterId, supplyId }, 62 | data: body, 63 | }); 64 | return new ServerResponse( 65 | 200, 66 | 'Successfully updated shelter supply', 67 | data, 68 | ); 69 | } catch (err: any) { 70 | this.logger.error(`Failed to update shelter supply: ${err}`); 71 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/shelter-supply/shelter-supply.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ShelterSupplyService } from './shelter-supply.service'; 4 | import { ShelterSupplyController } from './shelter-supply.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [ShelterSupplyService], 10 | controllers: [ShelterSupplyController], 11 | }) 12 | export class ShelterSupplyModule {} 13 | -------------------------------------------------------------------------------- /src/shelter-supply/shelter-supply.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ShelterSupplyService } from './shelter-supply.service'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | describe('ShelterSupplyService', () => { 6 | let service: ShelterSupplyService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ShelterSupplyService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(ShelterSupplyService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/shelter-supply/shelter-supply.service.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { CreateShelterSupplySchema, UpdateShelterSupplySchema } from './types'; 6 | import { SupplyPriority } from '../supply/types'; 7 | 8 | @Injectable() 9 | export class ShelterSupplyService { 10 | constructor(private readonly prismaService: PrismaService) {} 11 | 12 | private async handleUpdateShelterSum( 13 | shelterId: string, 14 | oldPriority: number, 15 | newPriority: number, 16 | ) { 17 | await this.prismaService.shelter.update({ 18 | where: { 19 | id: shelterId, 20 | }, 21 | data: { 22 | prioritySum: { 23 | increment: newPriority - oldPriority, 24 | }, 25 | updatedAt: new Date().toISOString(), 26 | }, 27 | }); 28 | } 29 | 30 | async store(body: z.infer) { 31 | const { shelterId, priority, supplyId, quantity } = 32 | CreateShelterSupplySchema.parse(body); 33 | await this.handleUpdateShelterSum(shelterId, 0, priority); 34 | await this.prismaService.shelterSupply.create({ 35 | data: { 36 | shelterId, 37 | priority, 38 | supplyId, 39 | quantity: priority !== SupplyPriority.UnderControl ? quantity : null, 40 | createdAt: new Date().toISOString(), 41 | }, 42 | }); 43 | } 44 | 45 | async update(body: z.infer) { 46 | const { data, where } = UpdateShelterSupplySchema.parse(body); 47 | const { priority, quantity } = data; 48 | if (priority !== null && priority !== undefined) { 49 | const shelterSupply = await this.prismaService.shelterSupply.findFirst({ 50 | where: { 51 | shelterId: where.shelterId, 52 | supplyId: where.supplyId, 53 | }, 54 | select: { 55 | priority: true, 56 | }, 57 | }); 58 | if (shelterSupply) 59 | await this.handleUpdateShelterSum( 60 | where.shelterId, 61 | shelterSupply.priority, 62 | priority, 63 | ); 64 | } 65 | 66 | await this.prismaService.shelterSupply.update({ 67 | where: { 68 | shelterId_supplyId: where, 69 | }, 70 | data: { 71 | ...data, 72 | quantity: priority !== SupplyPriority.UnderControl ? quantity : null, 73 | updatedAt: new Date().toISOString(), 74 | }, 75 | }); 76 | } 77 | 78 | async index(shelterId: string) { 79 | return await this.prismaService.shelterSupply.findMany({ 80 | where: { 81 | shelterId, 82 | }, 83 | select: { 84 | priority: true, 85 | quantity: true, 86 | supply: { 87 | select: { 88 | id: true, 89 | name: true, 90 | measure: true, 91 | supplyCategory: { 92 | select: { 93 | id: true, 94 | name: true, 95 | }, 96 | }, 97 | updatedAt: true, 98 | createdAt: true, 99 | }, 100 | }, 101 | createdAt: true, 102 | updatedAt: true, 103 | }, 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/shelter-supply/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | import { SupplyPriority } from '../supply/types'; 4 | 5 | const ShelterSupplySchema = z.object({ 6 | id: z.string(), 7 | shelterId: z.string(), 8 | supplyId: z.string(), 9 | priority: z.union([ 10 | z.literal(SupplyPriority.UnderControl), 11 | z.literal(SupplyPriority.Remaining), 12 | z.literal(SupplyPriority.Needing), 13 | z.literal(SupplyPriority.Urgent), 14 | ]), 15 | quantity: z.number().min(0).nullable().optional(), 16 | createdAt: z.string(), 17 | updatedAt: z.string().nullable().optional(), 18 | }); 19 | 20 | const CreateShelterSupplySchema = ShelterSupplySchema.pick({ 21 | shelterId: true, 22 | supplyId: true, 23 | priority: true, 24 | quantity: true, 25 | }); 26 | 27 | const UpdateShelterSupplySchema = z.object({ 28 | data: z 29 | .object({ 30 | priority: z.union([ 31 | z.literal(SupplyPriority.UnderControl), 32 | z.literal(SupplyPriority.Remaining), 33 | z.literal(SupplyPriority.Needing), 34 | z.literal(SupplyPriority.Urgent), 35 | ]), 36 | quantity: z.number().nullable().optional(), 37 | shelterId: z.string(), 38 | supplyId: z.string(), 39 | }) 40 | .partial(), 41 | where: z.object({ 42 | shelterId: z.string(), 43 | supplyId: z.string(), 44 | }), 45 | }); 46 | 47 | export { 48 | ShelterSupplySchema, 49 | CreateShelterSupplySchema, 50 | UpdateShelterSupplySchema, 51 | }; 52 | -------------------------------------------------------------------------------- /src/shelter/ShelterSearch.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@prisma/client'; 2 | 3 | import { calculateGeolocationBounds } from '@/utils/utils'; 4 | import { SupplyPriority } from 'src/supply/types'; 5 | import { PrismaService } from '../prisma/prisma.service'; 6 | import { 7 | SearchShelterTagResponse, 8 | ShelterSearchProps, 9 | ShelterStatus, 10 | ShelterTagInfo, 11 | ShelterTagType, 12 | } from './types/search.types'; 13 | 14 | const defaultTagsData: ShelterTagInfo = { 15 | NeedDonations: true, 16 | NeedVolunteers: true, 17 | RemainingSupplies: true, 18 | }; 19 | 20 | class ShelterSearch { 21 | private formProps: Partial; 22 | private prismaService: PrismaService; 23 | 24 | constructor( 25 | prismaService: PrismaService, 26 | props: Partial = {}, 27 | ) { 28 | this.prismaService = prismaService; 29 | this.formProps = { ...props }; 30 | this.getQuery = this.getQuery.bind(this); 31 | } 32 | 33 | priority(supplyIds: string[] = []): Prisma.ShelterWhereInput { 34 | if (!this.formProps.priorities?.length) return {}; 35 | 36 | return { 37 | shelterSupplies: { 38 | some: { 39 | priority: { 40 | in: this.formProps.priorities, 41 | }, 42 | supplyId: 43 | supplyIds.length > 0 44 | ? { 45 | in: supplyIds, 46 | } 47 | : undefined, 48 | }, 49 | }, 50 | }; 51 | } 52 | 53 | get shelterStatus(): Prisma.ShelterWhereInput[] { 54 | if (!this.formProps.shelterStatus) return []; 55 | 56 | const clausesFromStatus: Record< 57 | ShelterStatus, 58 | Prisma.ShelterWhereInput['capacity'] | null 59 | > = { 60 | waiting: null, 61 | available: { 62 | gt: this.prismaService.shelter.fields.shelteredPeople, 63 | }, 64 | unavailable: { 65 | lte: this.prismaService.shelter.fields.shelteredPeople, 66 | not: 0, 67 | }, 68 | }; 69 | 70 | return this.formProps.shelterStatus.map((status) => ({ 71 | capacity: clausesFromStatus[status], 72 | })); 73 | } 74 | 75 | supplyCategoryIds( 76 | priority?: SupplyPriority[] | null, 77 | ): Prisma.ShelterWhereInput { 78 | if (!this.formProps.supplyCategoryIds) return {}; 79 | 80 | if (!priority || !priority.length) { 81 | return { 82 | shelterSupplies: { 83 | some: { 84 | supply: { 85 | supplyCategoryId: { 86 | in: this.formProps.supplyCategoryIds, 87 | }, 88 | }, 89 | }, 90 | }, 91 | }; 92 | } 93 | 94 | return { 95 | shelterSupplies: { 96 | some: { 97 | priority: { 98 | in: priority, 99 | }, 100 | supply: { 101 | supplyCategoryId: { 102 | in: this.formProps.supplyCategoryIds, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }; 108 | } 109 | 110 | get supplyIds(): Prisma.ShelterWhereInput { 111 | if (!this.formProps.supplyIds) return {}; 112 | return { 113 | shelterSupplies: { 114 | some: { 115 | supply: { 116 | id: { 117 | in: this.formProps.supplyIds, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }; 123 | } 124 | 125 | async getSearch(): Promise { 126 | if (!this.formProps.search) return {}; 127 | 128 | const search = `${this.formProps.search.toLowerCase()}`; 129 | 130 | const results = await this.prismaService.$queryRaw<{ id: string }[]>( 131 | Prisma.sql`SELECT id FROM shelters WHERE lower(unaccent(address)) LIKE '%' || unaccent(${search}) || '%' OR lower(unaccent(name)) LIKE '%' || unaccent(${search}) || '%';`, 132 | ); 133 | 134 | return { 135 | id: { 136 | in: results.map((r) => r.id), 137 | }, 138 | }; 139 | } 140 | 141 | get cities(): Prisma.ShelterWhereInput { 142 | if (!this.formProps.cities) return {}; 143 | 144 | return { 145 | city: { 146 | in: this.formProps.cities, 147 | }, 148 | }; 149 | } 150 | 151 | get geolocation(): Prisma.ShelterWhereInput { 152 | if (!this.formProps.geolocation) return {}; 153 | 154 | const { minLat, maxLat, minLong, maxLong } = calculateGeolocationBounds( 155 | this.formProps.geolocation, 156 | ); 157 | 158 | return { 159 | latitude: { 160 | gte: minLat, 161 | lte: maxLat, 162 | }, 163 | longitude: { 164 | gte: minLong, 165 | lte: maxLong, 166 | }, 167 | }; 168 | } 169 | 170 | async getQuery(): Promise { 171 | if (Object.keys(this.formProps).length === 0) return {}; 172 | 173 | const search = await this.getSearch(); 174 | const queryData = { 175 | AND: [ 176 | this.cities, 177 | this.geolocation, 178 | search, 179 | { OR: this.shelterStatus }, 180 | this.priority(this.formProps.supplyIds), 181 | this.supplyCategoryIds(this.formProps.priorities), 182 | ], 183 | }; 184 | 185 | return queryData; 186 | } 187 | } 188 | 189 | /** 190 | * 191 | * @param formProps Uma interface do tipo ShelterTagInfo | null. Que indica a quantidade máxima de cada categoria deverá ser retornada 192 | * @param results Resultado da query em `this.prismaService.shelter.findMany` 193 | * @param voluntaryIds 194 | * @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 195 | */ 196 | function parseTagResponse( 197 | tagProps: Partial> = {}, 198 | results: SearchShelterTagResponse[], 199 | voluntaryIds: string[], 200 | ): SearchShelterTagResponse[] { 201 | const tags: ShelterTagInfo = { 202 | ...defaultTagsData, 203 | ...(tagProps?.tags ?? {}), 204 | }; 205 | 206 | const parsed = results.map((result) => { 207 | return { 208 | ...result, 209 | shelterSupplies: result.shelterSupplies.reduce((prev, shelterSupply) => { 210 | const supplyTags: ShelterTagType[] = []; 211 | if ( 212 | tags.NeedDonations && 213 | [SupplyPriority.Needing, SupplyPriority.Urgent].includes( 214 | shelterSupply.priority, 215 | ) 216 | ) { 217 | supplyTags.push('NeedDonations'); 218 | } 219 | if ( 220 | tags.NeedVolunteers && 221 | voluntaryIds.includes(shelterSupply.supply.supplyCategoryId) && 222 | [SupplyPriority.Urgent, SupplyPriority.Needing].includes( 223 | shelterSupply.priority, 224 | ) 225 | ) { 226 | supplyTags.push('NeedVolunteers'); 227 | } 228 | if ( 229 | tags.RemainingSupplies && 230 | [SupplyPriority.Remaining].includes(shelterSupply.priority) 231 | ) { 232 | supplyTags.push('RemainingSupplies'); 233 | } 234 | return [...prev, { ...shelterSupply, tags: supplyTags }]; 235 | }, [] as any), 236 | }; 237 | }); 238 | return parsed; 239 | } 240 | 241 | export { ShelterSearch, parseTagResponse }; 242 | -------------------------------------------------------------------------------- /src/shelter/shelter.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { ShelterController } from './shelter.controller'; 4 | import { ShelterService } from './shelter.service'; 5 | 6 | describe('ShelterController', () => { 7 | let controller: ShelterController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [ShelterController], 12 | providers: [ShelterService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return { 17 | supplyCategory: { findMany: jest.fn().mockResolvedValue(0) }, 18 | }; 19 | } 20 | }) 21 | .compile(); 22 | 23 | controller = module.get(ShelterController); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/shelter/shelter.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | UseGuards, 12 | } from '@nestjs/common'; 13 | import { ApiTags } from '@nestjs/swagger'; 14 | 15 | import { ShelterService } from './shelter.service'; 16 | import { ServerResponse } from '../utils'; 17 | import { StaffGuard } from '@/guards/staff.guard'; 18 | import { ApplyUser } from '@/guards/apply-user.guard'; 19 | import { UserDecorator } from '@/decorators/UserDecorator/user.decorator'; 20 | 21 | @ApiTags('Abrigos') 22 | @Controller('shelters') 23 | export class ShelterController { 24 | private logger = new Logger(ShelterController.name); 25 | 26 | constructor(private readonly shelterService: ShelterService) {} 27 | 28 | @Get('') 29 | async index(@Query() query) { 30 | try { 31 | const data = await this.shelterService.index(query); 32 | return new ServerResponse(200, 'Successfully get shelters', data); 33 | } catch (err: any) { 34 | this.logger.error(`Failed to get shelters: ${err}`); 35 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 36 | } 37 | } 38 | 39 | @Get('cities') 40 | async cities() { 41 | try { 42 | const data = await this.shelterService.getCities(); 43 | return new ServerResponse(200, 'Successfully get shelters cities', data); 44 | } catch (err: any) { 45 | this.logger.error(`Failed to get shelters cities: ${err}`); 46 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 47 | } 48 | } 49 | 50 | @Get(':id') 51 | @UseGuards(ApplyUser) 52 | async show(@UserDecorator() user: any, @Param('id') id: string) { 53 | try { 54 | const isLogged = 55 | Boolean(user) && Boolean(user?.sessionId) && Boolean(user?.userId); 56 | const data = await this.shelterService.show(id, isLogged); 57 | return new ServerResponse(200, 'Successfully get shelter', data); 58 | } catch (err: any) { 59 | this.logger.error(`Failed to get shelter: ${err}`); 60 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 61 | } 62 | } 63 | 64 | @Post('') 65 | @UseGuards(StaffGuard) 66 | async store(@Body() body) { 67 | try { 68 | const data = await this.shelterService.store(body); 69 | return new ServerResponse(200, 'Successfully created shelter', data); 70 | } catch (err: any) { 71 | this.logger.error(`Failed to create shelter: ${err}`); 72 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 73 | } 74 | } 75 | 76 | @Put(':id') 77 | async update(@Param('id') id: string, @Body() body) { 78 | try { 79 | const data = await this.shelterService.update(id, body); 80 | return new ServerResponse(200, 'Successfully updated shelter', data); 81 | } catch (err: any) { 82 | this.logger.error(`Failed update shelter: ${err}`); 83 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 84 | } 85 | } 86 | 87 | @Put(':id/admin') 88 | @UseGuards(StaffGuard) 89 | async fullUpdate(@Param('id') id: string, @Body() body) { 90 | try { 91 | const data = await this.shelterService.fullUpdate(id, body); 92 | return new ServerResponse(200, 'Successfully updated shelter', data); 93 | } catch (err: any) { 94 | this.logger.error(`Failed to update shelter: ${err}`); 95 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/shelter/shelter.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ShelterService } from './shelter.service'; 4 | import { ShelterController } from './shelter.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [ShelterService], 10 | controllers: [ShelterController], 11 | }) 12 | export class ShelterModule {} 13 | -------------------------------------------------------------------------------- /src/shelter/shelter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ShelterService } from './shelter.service'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | describe('ShelterService', () => { 6 | let service: ShelterService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ShelterService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return { 15 | supplyCategory: { 16 | findMany: jest.fn().mockResolvedValue([]), 17 | }, 18 | }; 19 | } 20 | }) 21 | .compile(); 22 | 23 | service = module.get(ShelterService); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(service).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/shelter/shelter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; 2 | import { Prisma, ShelterSupply } from '@prisma/client'; 3 | import { DefaultArgs } from '@prisma/client/runtime/library'; 4 | import { millisecondsToHours, subDays } from 'date-fns'; 5 | import * as qs from 'qs'; 6 | import { z } from 'zod'; 7 | 8 | import { PrismaService } from '../prisma/prisma.service'; 9 | import { SupplyPriority } from '../supply/types'; 10 | import { SearchSchema } from '../types'; 11 | import { ShelterSearch, parseTagResponse } from './ShelterSearch'; 12 | import { ShelterSearchPropsSchema } from './types/search.types'; 13 | import { 14 | CreateShelterSchema, 15 | FullUpdateShelterSchema, 16 | IShelterSupplyDecay, 17 | UpdateShelterSchema, 18 | } from './types/types'; 19 | import { registerSupplyLog } from '@/interceptors/interceptors/shelter-supply-history/utils'; 20 | 21 | @Injectable() 22 | export class ShelterService implements OnModuleInit { 23 | private logger = new Logger(ShelterService.name); 24 | private voluntaryIds: string[] = []; 25 | 26 | constructor(private readonly prismaService: PrismaService) {} 27 | 28 | onModuleInit() { 29 | this.loadVoluntaryIds(); 30 | } 31 | 32 | async store(body: z.infer) { 33 | const payload = CreateShelterSchema.parse(body); 34 | 35 | await this.prismaService.shelter.create({ 36 | data: { 37 | ...payload, 38 | createdAt: new Date().toISOString(), 39 | updatedAt: subDays(new Date(), 1).toISOString(), 40 | }, 41 | }); 42 | } 43 | 44 | async update(id: string, body: z.infer) { 45 | const payload = UpdateShelterSchema.parse(body); 46 | await this.prismaService.shelter.update({ 47 | where: { 48 | id, 49 | }, 50 | data: { 51 | ...payload, 52 | updatedAt: new Date().toISOString(), 53 | }, 54 | }); 55 | } 56 | 57 | async fullUpdate(id: string, body: z.infer) { 58 | const payload = FullUpdateShelterSchema.parse(body); 59 | await this.prismaService.shelter.update({ 60 | where: { 61 | id, 62 | }, 63 | data: { 64 | ...payload, 65 | updatedAt: new Date().toISOString(), 66 | }, 67 | }); 68 | } 69 | 70 | async show(id: string, shouldShowContact: boolean) { 71 | const data = await this.prismaService.shelter.findFirst({ 72 | where: { 73 | id, 74 | }, 75 | select: { 76 | id: true, 77 | name: true, 78 | address: true, 79 | street: true, 80 | neighbourhood: true, 81 | city: true, 82 | streetNumber: true, 83 | zipCode: true, 84 | pix: true, 85 | shelteredPeople: true, 86 | capacity: true, 87 | contact: shouldShowContact, 88 | petFriendly: true, 89 | shelteredPets: true, 90 | petsCapacity: true, 91 | prioritySum: true, 92 | latitude: true, 93 | longitude: true, 94 | verified: true, 95 | actived: true, 96 | category: true, 97 | shelterSupplies: { 98 | select: { 99 | priority: true, 100 | quantity: true, 101 | supplyId: true, 102 | shelterId: true, 103 | createdAt: true, 104 | updatedAt: true, 105 | supply: { 106 | select: { 107 | id: true, 108 | name: true, 109 | measure: true, 110 | supplyCategory: { 111 | select: { 112 | id: true, 113 | name: true, 114 | }, 115 | }, 116 | createdAt: true, 117 | updatedAt: true, 118 | }, 119 | }, 120 | }, 121 | }, 122 | createdAt: true, 123 | updatedAt: true, 124 | }, 125 | }); 126 | 127 | if (data) this.decayShelterSupply(data.shelterSupplies); 128 | 129 | return data; 130 | } 131 | 132 | async index(query: any) { 133 | const { 134 | order, 135 | orderBy, 136 | page, 137 | perPage, 138 | search: searchQuery, 139 | } = SearchSchema.parse(query); 140 | const queryData = ShelterSearchPropsSchema.parse(qs.parse(searchQuery)); 141 | const { getQuery } = new ShelterSearch(this.prismaService, queryData); 142 | const where = await getQuery(); 143 | 144 | const count = await this.prismaService.shelter.count({ where }); 145 | 146 | const take = perPage; 147 | const skip = perPage * (page - 1); 148 | 149 | const whereData: Prisma.ShelterFindManyArgs = { 150 | take, 151 | skip, 152 | orderBy: { [orderBy]: order }, 153 | where, 154 | }; 155 | 156 | const results = await this.prismaService.shelter.findMany({ 157 | ...whereData, 158 | select: { 159 | id: true, 160 | name: true, 161 | pix: true, 162 | address: true, 163 | street: true, 164 | neighbourhood: true, 165 | city: true, 166 | streetNumber: true, 167 | zipCode: true, 168 | capacity: true, 169 | petFriendly: true, 170 | shelteredPets: true, 171 | petsCapacity: true, 172 | shelteredPeople: true, 173 | prioritySum: true, 174 | verified: true, 175 | latitude: true, 176 | longitude: true, 177 | actived: true, 178 | category: true, 179 | createdAt: true, 180 | updatedAt: true, 181 | shelterSupplies: { 182 | where: { 183 | priority: { 184 | notIn: [SupplyPriority.UnderControl], 185 | }, 186 | }, 187 | orderBy: { 188 | updatedAt: 'desc', 189 | }, 190 | include: { 191 | supply: true, 192 | }, 193 | }, 194 | }, 195 | }); 196 | 197 | this.decayShelterSupply(results.flatMap((r) => r.shelterSupplies)); 198 | 199 | const parsed = parseTagResponse(queryData, results, this.voluntaryIds); 200 | 201 | return { 202 | page, 203 | perPage, 204 | count, 205 | results: parsed, 206 | }; 207 | } 208 | 209 | async getCities() { 210 | const cities = await this.prismaService.shelter.groupBy({ 211 | where: { 212 | city: { 213 | not: null, 214 | }, 215 | }, 216 | by: ['city'], 217 | _count: { 218 | id: true, 219 | }, 220 | orderBy: { 221 | _count: { 222 | id: 'desc', 223 | }, 224 | }, 225 | }); 226 | 227 | return cities.map(({ city, _count: { id: sheltersCount } }) => ({ 228 | city, 229 | sheltersCount, 230 | })); 231 | } 232 | 233 | private loadVoluntaryIds() { 234 | this.prismaService.supplyCategory 235 | .findMany({ 236 | where: { 237 | name: { 238 | in: ['Especialistas e Profissionais', 'Voluntariado'], 239 | }, 240 | }, 241 | }) 242 | .then((resp) => { 243 | this.voluntaryIds.push(...resp.map((s) => s.id)); 244 | }); 245 | } 246 | 247 | private parseShelterSupply( 248 | shelterSupply: ShelterSupply, 249 | ): IShelterSupplyDecay { 250 | return { 251 | shelterId: shelterSupply.shelterId, 252 | supplyId: shelterSupply.supplyId, 253 | priority: shelterSupply.priority, 254 | createdAt: new Date(shelterSupply.createdAt).getTime(), 255 | updatedAt: shelterSupply.updatedAt 256 | ? new Date(shelterSupply.updatedAt).getTime() 257 | : 0, 258 | }; 259 | } 260 | 261 | private canDecayShelterSupply( 262 | shelterSupply: IShelterSupplyDecay, 263 | priorities: SupplyPriority[], 264 | timeInHoursToDecay: number, 265 | ): boolean { 266 | return ( 267 | priorities.includes(shelterSupply.priority) && 268 | millisecondsToHours( 269 | new Date().getTime() - 270 | Math.max(shelterSupply.createdAt, shelterSupply.updatedAt), 271 | ) > timeInHoursToDecay 272 | ); 273 | } 274 | 275 | private async handleDecayShelterSupply( 276 | shelterSupplies: IShelterSupplyDecay[], 277 | newPriority: SupplyPriority, 278 | ) { 279 | const shelterIds: Set = new Set(); 280 | shelterSupplies.forEach((s) => shelterIds.add(s.shelterId)); 281 | 282 | await this.prismaService.$transaction([ 283 | this.prismaService.shelter.updateMany({ 284 | where: { 285 | id: { 286 | in: Array.from(shelterIds), 287 | }, 288 | }, 289 | data: { 290 | updatedAt: new Date().toISOString(), 291 | }, 292 | }), 293 | ...shelterSupplies.map((s) => 294 | this.prismaService.shelterSupply.update({ 295 | where: { 296 | shelterId_supplyId: { 297 | shelterId: s.shelterId, 298 | supplyId: s.supplyId, 299 | }, 300 | }, 301 | data: { 302 | priority: newPriority, 303 | updatedAt: new Date().toISOString(), 304 | }, 305 | }), 306 | ), 307 | ]); 308 | 309 | shelterSupplies.forEach((s) => { 310 | registerSupplyLog({ 311 | shelterId: s.shelterId, 312 | supplyId: s.supplyId, 313 | priority: newPriority, 314 | }); 315 | }); 316 | } 317 | 318 | private async decayShelterSupply(shelterSupplies: ShelterSupply[]) { 319 | this.handleDecayShelterSupply( 320 | shelterSupplies 321 | .map(this.parseShelterSupply) 322 | .filter((f) => 323 | this.canDecayShelterSupply(f, [SupplyPriority.Urgent], 48), 324 | ), 325 | 326 | SupplyPriority.Needing, 327 | ); 328 | 329 | this.handleDecayShelterSupply( 330 | shelterSupplies 331 | .map(this.parseShelterSupply) 332 | .filter((f) => 333 | this.canDecayShelterSupply( 334 | f, 335 | [SupplyPriority.Needing, SupplyPriority.Remaining], 336 | 72, 337 | ), 338 | ), 339 | SupplyPriority.UnderControl, 340 | ); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/shelter/types/search.types.ts: -------------------------------------------------------------------------------- 1 | import { Shelter, ShelterSupply, Supply } from '@prisma/client'; 2 | import { z } from 'zod'; 3 | import { SupplyPriority } from '../../supply/types'; 4 | 5 | const ShelterStatusSchema = z.enum(['available', 'unavailable', 'waiting']); 6 | 7 | export type ShelterStatus = z.infer; 8 | 9 | const ShelterTagTypeSchema = z.enum([ 10 | 'NeedVolunteers', 11 | 'NeedDonations', 12 | 'RemainingSupplies', 13 | ]); 14 | 15 | const ShelterTagInfoSchema = z.record( 16 | ShelterTagTypeSchema, 17 | z.boolean().optional(), 18 | ); 19 | 20 | export type ShelterTagType = z.infer; 21 | 22 | export type ShelterTagInfo = z.infer; 23 | 24 | export const GeolocationFilterSchema = z.object({ 25 | latitude: z.coerce.number(), 26 | longitude: z.coerce.number(), 27 | radiusInMeters: z.coerce.number(), 28 | }); 29 | 30 | export type GeolocationFilter = z.infer; 31 | 32 | export const ShelterSearchPropsSchema = z.object({ 33 | search: z.string().optional(), 34 | priorities: z 35 | .array(z.string()) 36 | .optional() 37 | .transform((values) => 38 | values ? values.map(parseInt).filter((v) => !isNaN(v)) : [], 39 | ) 40 | .pipe(z.array(z.nativeEnum(SupplyPriority))), 41 | supplyCategoryIds: z.array(z.string()).optional(), 42 | supplyIds: z.array(z.string()).optional(), 43 | shelterStatus: z.array(ShelterStatusSchema).optional(), 44 | tags: ShelterTagInfoSchema.nullable().optional(), 45 | cities: z.array(z.string()).optional(), 46 | geolocation: GeolocationFilterSchema.optional(), 47 | }); 48 | 49 | export type ShelterSearchProps = z.infer; 50 | 51 | type AllowedShelterFields = Omit; 52 | 53 | export type SearchShelterTagResponse = AllowedShelterFields & { 54 | shelterSupplies: (ShelterSupply & { supply: Supply })[]; 55 | }; 56 | -------------------------------------------------------------------------------- /src/shelter/types/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | import { capitalize } from '../../utils'; 4 | import { removeEmptyStrings } from '@/utils/utils'; 5 | 6 | export interface DefaultSupplyProps { 7 | category: string; 8 | supply: string; 9 | } 10 | 11 | const ShelterSchema = z.object({ 12 | id: z.string(), 13 | name: z.string().transform(capitalize), 14 | pix: z.string().nullable().optional(), 15 | address: z.string().transform(capitalize), 16 | city: z.string().transform(capitalize).nullable().optional(), 17 | neighbourhood: z.string().transform(capitalize).nullable().optional(), 18 | street: z.string().transform(capitalize).nullable().optional(), 19 | streetNumber: z.string().nullable().optional(), 20 | zipCode: z.string().nullable().optional(), 21 | petFriendly: z.boolean().nullable().optional(), 22 | shelteredPets: z.number().min(0).nullable().optional(), 23 | petsCapacity: z.number().min(0).nullable().optional(), 24 | shelteredPeople: z.number().min(0).nullable().optional(), 25 | latitude: z.number().nullable().optional(), 26 | longitude: z.number().nullable().optional(), 27 | capacity: z.number().min(0).nullable().optional(), 28 | contact: z.string().nullable().optional(), 29 | verified: z.boolean(), 30 | createdAt: z.string(), 31 | updatedAt: z.string().nullable().optional(), 32 | }); 33 | 34 | const CreateShelterSchema = ShelterSchema.omit({ 35 | id: true, 36 | createdAt: true, 37 | updatedAt: true, 38 | verified: true, 39 | }); 40 | 41 | const UpdateShelterSchema = ShelterSchema.pick({ 42 | petFriendly: true, 43 | shelteredPeople: true, 44 | shelteredPets: true, 45 | petsCapacity: true, 46 | }).partial(); 47 | 48 | const FullUpdateShelterSchema = ShelterSchema.omit({ 49 | id: true, 50 | createdAt: true, 51 | updatedAt: true, 52 | }) 53 | .partial() 54 | .transform((args) => removeEmptyStrings(args)); 55 | 56 | export interface IShelterSupplyDecay { 57 | shelterId: string; 58 | supplyId: string; 59 | priority: number; 60 | createdAt: number; 61 | updatedAt: number; 62 | } 63 | 64 | export { 65 | ShelterSchema, 66 | CreateShelterSchema, 67 | UpdateShelterSchema, 68 | FullUpdateShelterSchema, 69 | }; 70 | -------------------------------------------------------------------------------- /src/supplies-history/supplies-history.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { SuppliesHistoryController } from './supplies-history.controller'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { SuppliesHistoryService } from './supplies-history.service'; 6 | 7 | describe('SuppliesHistoryController', () => { 8 | let controller: SuppliesHistoryController; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | providers: [SuppliesHistoryService], 13 | controllers: [SuppliesHistoryController], 14 | }) 15 | .useMocker((token) => { 16 | if (token === PrismaService) { 17 | return {}; 18 | } 19 | }) 20 | .compile(); 21 | 22 | controller = module.get( 23 | SuppliesHistoryController, 24 | ); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/supplies-history/supplies-history.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | HttpException, 5 | Logger, 6 | Param, 7 | Query, 8 | } from '@nestjs/common'; 9 | import { SuppliesHistoryService } from './supplies-history.service'; 10 | import { ServerResponse } from '../utils'; 11 | 12 | @Controller('supplies/history') 13 | export class SuppliesHistoryController { 14 | private logger = new Logger(SuppliesHistoryController.name); 15 | 16 | constructor( 17 | private readonly suppliesHistoryService: SuppliesHistoryService, 18 | ) {} 19 | 20 | @Get(':shelterId') 21 | async index(@Param('shelterId') shelterId: string, @Query() query) { 22 | try { 23 | const data = await this.suppliesHistoryService.index(shelterId, query); 24 | return new ServerResponse(200, 'Successfully get supplies history', data); 25 | } catch (err: any) { 26 | this.logger.error(`Failed to get supplies history: ${err}`); 27 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/supplies-history/supplies-history.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { SuppliesHistoryService } from './supplies-history.service'; 4 | import { SuppliesHistoryController } from './supplies-history.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [SuppliesHistoryService], 10 | controllers: [SuppliesHistoryController], 11 | exports: [SuppliesHistoryService], 12 | }) 13 | export class SuppliesHistoryModule {} 14 | -------------------------------------------------------------------------------- /src/supplies-history/supplies-history.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SuppliesHistoryService } from './supplies-history.service'; 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | 5 | describe('SuppliesHistoryService', () => { 6 | let service: SuppliesHistoryService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [SuppliesHistoryService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(SuppliesHistoryService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/supplies-history/supplies-history.service.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { Prisma } from '@prisma/client'; 4 | import { DefaultArgs } from '@prisma/client/runtime/library'; 5 | 6 | import { PrismaService } from '../prisma/prisma.service'; 7 | import { CreateSupplyHistorySchema } from './types'; 8 | import { SearchSchema } from '../types'; 9 | 10 | @Injectable() 11 | export class SuppliesHistoryService { 12 | constructor(private readonly prismaService: PrismaService) {} 13 | 14 | async index(shelterId: string, query: any) { 15 | const { order, orderBy, page, perPage } = SearchSchema.parse(query); 16 | 17 | const where: Prisma.SupplyHistoryWhereInput = { 18 | shelterId, 19 | }; 20 | 21 | const count = await this.prismaService.supplyHistory.count({ where }); 22 | 23 | const take = perPage; 24 | const skip = perPage * (page - 1); 25 | 26 | const whereData: Prisma.SupplyHistoryFindManyArgs = { 27 | take, 28 | skip, 29 | orderBy: { [orderBy]: order }, 30 | where, 31 | }; 32 | 33 | const results = await this.prismaService.supplyHistory.findMany({ 34 | ...whereData, 35 | select: { 36 | id: true, 37 | supply: { 38 | select: { 39 | measure: true, 40 | name: true, 41 | }, 42 | }, 43 | priority: true, 44 | quantity: true, 45 | predecessor: { 46 | select: { 47 | priority: true, 48 | quantity: true, 49 | }, 50 | }, 51 | createdAt: true, 52 | }, 53 | orderBy: { 54 | createdAt: 'desc', 55 | }, 56 | }); 57 | 58 | return { 59 | page, 60 | perPage, 61 | count, 62 | results, 63 | }; 64 | } 65 | 66 | async store(body: z.infer) { 67 | const { shelterId, supplyId, ...rest } = 68 | CreateSupplyHistorySchema.parse(body); 69 | 70 | const prev = await this.prismaService.supplyHistory.findFirst({ 71 | where: { 72 | shelterId, 73 | supplyId, 74 | }, 75 | orderBy: { 76 | createdAt: 'desc', 77 | }, 78 | }); 79 | 80 | await this.prismaService.supplyHistory.create({ 81 | data: { 82 | shelterId, 83 | supplyId, 84 | ...rest, 85 | createdAt: new Date().toISOString(), 86 | predecessorId: prev?.id, 87 | }, 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/supplies-history/types.ts: -------------------------------------------------------------------------------- 1 | import { removeEmptyStrings } from '@/utils/utils'; 2 | import { z } from 'zod'; 3 | 4 | const SupplyHistorySchema = z.object({ 5 | id: z.string(), 6 | successorId: z.string().nullish(), 7 | shelterId: z.string(), 8 | supplyId: z.string(), 9 | priority: z.number().nullish(), 10 | quantity: z.number().nullish(), 11 | createdAt: z.string(), 12 | }); 13 | 14 | const CreateSupplyHistorySchema = SupplyHistorySchema.omit({ 15 | id: true, 16 | successorId: true, 17 | createdAt: true, 18 | }).transform((args) => removeEmptyStrings(args)); 19 | 20 | export { SupplyHistorySchema, CreateSupplyHistorySchema }; 21 | -------------------------------------------------------------------------------- /src/supply-categories/supply-categories.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SupplyCategoriesController } from './supply-categories.controller'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { SupplyCategoriesService } from './supply-categories.service'; 5 | 6 | describe('SupplyCategoriesController', () => { 7 | let controller: SupplyCategoriesController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [SupplyCategoriesController], 12 | providers: [SupplyCategoriesService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return {}; 17 | } 18 | }) 19 | .compile(); 20 | 21 | controller = module.get( 22 | SupplyCategoriesController, 23 | ); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(controller).toBeDefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/supply-categories/supply-categories.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { ApiTags } from '@nestjs/swagger'; 13 | 14 | import { SupplyCategoriesService } from './supply-categories.service'; 15 | import { ServerResponse } from '../utils'; 16 | import { AdminGuard } from '@/guards/admin.guard'; 17 | 18 | @ApiTags('Categoria de Suprimentos') 19 | @Controller('supply-categories') 20 | export class SupplyCategoriesController { 21 | private logger = new Logger(SupplyCategoriesController.name); 22 | 23 | constructor( 24 | private readonly supplyCategoryServices: SupplyCategoriesService, 25 | ) {} 26 | 27 | @Get('') 28 | async index() { 29 | try { 30 | const data = await this.supplyCategoryServices.index(); 31 | return new ServerResponse( 32 | 200, 33 | 'Successfully get supply categories', 34 | data, 35 | ); 36 | } catch (err: any) { 37 | this.logger.error(`Failed to get supply categories: ${err}`); 38 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 39 | } 40 | } 41 | 42 | @Post('') 43 | @UseGuards(AdminGuard) 44 | async store(@Body() body) { 45 | try { 46 | const data = await this.supplyCategoryServices.store(body); 47 | return new ServerResponse( 48 | 200, 49 | 'Successfully created supply category', 50 | data, 51 | ); 52 | } catch (err: any) { 53 | this.logger.error(`Failed to create supply category: ${err}`); 54 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 55 | } 56 | } 57 | 58 | @Put(':id') 59 | @UseGuards(AdminGuard) 60 | async update(@Param('id') id: string, @Body() body) { 61 | try { 62 | const data = await this.supplyCategoryServices.update(id, body); 63 | return new ServerResponse( 64 | 200, 65 | 'Successfully updated supply category', 66 | data, 67 | ); 68 | } catch (err: any) { 69 | this.logger.error(`Failed to update supply category: ${err}`); 70 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/supply-categories/supply-categories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { SupplyCategoriesService } from './supply-categories.service'; 4 | import { SupplyCategoriesController } from './supply-categories.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [SupplyCategoriesService], 10 | controllers: [SupplyCategoriesController], 11 | }) 12 | export class SupplyCategoriesModule {} 13 | -------------------------------------------------------------------------------- /src/supply-categories/supply-categories.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { SupplyCategoriesService } from './supply-categories.service'; 4 | 5 | describe('SupplyCategoriesService', () => { 6 | let service: SupplyCategoriesService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [SupplyCategoriesService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(SupplyCategoriesService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/supply-categories/supply-categories.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | import { z } from 'zod'; 5 | import { 6 | CreateSupplyCategorySchema, 7 | UpdateSupplyCategorySchema, 8 | } from './types'; 9 | 10 | @Injectable() 11 | export class SupplyCategoriesService { 12 | constructor(private readonly prismaService: PrismaService) {} 13 | 14 | async store(body: z.infer) { 15 | const payload = CreateSupplyCategorySchema.parse(body); 16 | await this.prismaService.supplyCategory.create({ 17 | data: { 18 | ...payload, 19 | createdAt: new Date().toISOString(), 20 | }, 21 | }); 22 | } 23 | 24 | async update(id: string, body: z.infer) { 25 | const payload = UpdateSupplyCategorySchema.parse(body); 26 | await this.prismaService.supplyCategory.update({ 27 | where: { 28 | id, 29 | }, 30 | data: { 31 | ...payload, 32 | updatedAt: new Date().toISOString(), 33 | }, 34 | }); 35 | } 36 | 37 | async index() { 38 | return await this.prismaService.supplyCategory.findMany({}); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/supply-categories/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { capitalize } from '../utils'; 3 | 4 | export enum SupplyStatus { 5 | UnderControl = 'UnderControl', 6 | Remaining = 'Remaining', 7 | Needing = 'Needing', 8 | Urgent = 'Urgent', 9 | } 10 | 11 | const SupplyCategorySchema = z.object({ 12 | id: z.string(), 13 | name: z.string().transform(capitalize), 14 | createdAt: z.string(), 15 | updatedAt: z.string().nullable().optional(), 16 | }); 17 | 18 | const CreateSupplyCategorySchema = SupplyCategorySchema.pick({ 19 | name: true, 20 | }); 21 | 22 | const UpdateSupplyCategorySchema = SupplyCategorySchema.pick({ 23 | name: true, 24 | }).partial(); 25 | 26 | export { 27 | SupplyCategorySchema, 28 | CreateSupplyCategorySchema, 29 | UpdateSupplyCategorySchema, 30 | }; 31 | -------------------------------------------------------------------------------- /src/supply/supply.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SupplyController } from './supply.controller'; 3 | import { SupplyService } from './supply.service'; 4 | import { PrismaService } from 'src/prisma/prisma.service'; 5 | 6 | describe('SupplyController', () => { 7 | let controller: SupplyController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [SupplyController], 12 | providers: [SupplyService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return {}; 17 | } 18 | }) 19 | .compile(); 20 | 21 | controller = module.get(SupplyController); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(controller).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/supply/supply.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | } from '@nestjs/common'; 11 | import { ApiTags } from '@nestjs/swagger'; 12 | 13 | import { ServerResponse } from '../utils'; 14 | import { SupplyService } from './supply.service'; 15 | 16 | @ApiTags('Suprimentos') 17 | @Controller('supplies') 18 | export class SupplyController { 19 | private logger = new Logger(SupplyController.name); 20 | 21 | constructor(private readonly supplyServices: SupplyService) {} 22 | 23 | @Get('') 24 | async index() { 25 | try { 26 | const data = await this.supplyServices.index(); 27 | return new ServerResponse(200, 'Successfully get supplies', data); 28 | } catch (err: any) { 29 | this.logger.error(`Failed to get supplies: ${err}`); 30 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 31 | } 32 | } 33 | 34 | @Post('') 35 | async store(@Body() body) { 36 | try { 37 | const data = await this.supplyServices.store(body); 38 | return new ServerResponse(200, 'Successfully created supply', data); 39 | } catch (err: any) { 40 | this.logger.error(`Failed to create supply: ${err}`); 41 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 42 | } 43 | } 44 | 45 | @Put(':id') 46 | async update(@Param('id') id: string, @Body() body) { 47 | try { 48 | const data = await this.supplyServices.update(id, body); 49 | return new ServerResponse(200, 'Successfully updated supply', data); 50 | } catch (err: any) { 51 | this.logger.error(`Failed to update supply: ${err}`); 52 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/supply/supply.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { SupplyService } from './supply.service'; 4 | import { SupplyController } from './supply.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [SupplyService], 10 | controllers: [SupplyController], 11 | }) 12 | export class SupplyModule {} 13 | -------------------------------------------------------------------------------- /src/supply/supply.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SupplyService } from './supply.service'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | describe('SupplyService', () => { 6 | let service: SupplyService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [SupplyService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(SupplyService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/supply/supply.service.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { CreateSupplySchema, UpdateSupplySchema } from './types'; 6 | 7 | @Injectable() 8 | export class SupplyService { 9 | constructor(private readonly prismaService: PrismaService) {} 10 | 11 | async store(body: z.infer) { 12 | const payload = CreateSupplySchema.parse(body); 13 | return await this.prismaService.supply.create({ 14 | data: { 15 | ...payload, 16 | createdAt: new Date().toISOString(), 17 | }, 18 | }); 19 | } 20 | 21 | async update(id: string, body: z.infer) { 22 | const payload = UpdateSupplySchema.parse(body); 23 | await this.prismaService.supply.update({ 24 | where: { 25 | id, 26 | }, 27 | data: { 28 | ...payload, 29 | updatedAt: new Date().toISOString(), 30 | }, 31 | }); 32 | } 33 | 34 | async index() { 35 | const data = await this.prismaService.supply.findMany({ 36 | distinct: ['name', 'supplyCategoryId'], 37 | orderBy: { 38 | name: 'desc', 39 | }, 40 | select: { 41 | id: true, 42 | name: true, 43 | supplyCategory: { 44 | select: { 45 | id: true, 46 | name: true, 47 | }, 48 | }, 49 | createdAt: true, 50 | updatedAt: true, 51 | }, 52 | }); 53 | 54 | return data; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/supply/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { capitalize } from '../utils'; 3 | 4 | enum SupplyPriority { 5 | UnderControl = 0, 6 | Remaining = 1, 7 | Needing = 10, 8 | Urgent = 100, 9 | } 10 | 11 | const SupplySchema = z.object({ 12 | id: z.string(), 13 | supplyCategoryId: z.string(), 14 | name: z.string().transform(capitalize), 15 | createdAt: z.string(), 16 | updatedAt: z.string().nullable().optional(), 17 | }); 18 | 19 | const CreateSupplySchema = SupplySchema.omit({ 20 | id: true, 21 | createdAt: true, 22 | updatedAt: true, 23 | }); 24 | 25 | const UpdateSupplySchema = SupplySchema.pick({ 26 | name: true, 27 | supplyCategoryId: true, 28 | }).partial(); 29 | 30 | export { SupplySchema, CreateSupplySchema, UpdateSupplySchema, SupplyPriority }; 31 | -------------------------------------------------------------------------------- /src/supporters/supporters.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { SupportersController } from './supporters.controller'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { SupportersService } from './supporters.service'; 6 | 7 | describe('SupportersController', () => { 8 | let controller: SupportersController; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | controllers: [SupportersController], 13 | providers: [SupportersService], 14 | }) 15 | .useMocker((token) => { 16 | if (token === PrismaService) { 17 | return { 18 | supplyCategory: { findMany: jest.fn().mockResolvedValue(0) }, 19 | }; 20 | } 21 | }) 22 | .compile(); 23 | 24 | controller = module.get(SupportersController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/supporters/supporters.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Post, 8 | UseGuards, 9 | } from '@nestjs/common'; 10 | 11 | import { SupportersService } from './supporters.service'; 12 | import { ServerResponse } from '../utils'; 13 | import { AdminGuard } from '@/guards/admin.guard'; 14 | 15 | @Controller('supporters') 16 | export class SupportersController { 17 | private logger = new Logger(SupportersController.name); 18 | 19 | constructor(private readonly supportersService: SupportersService) {} 20 | 21 | @Get('') 22 | async index() { 23 | try { 24 | const data = await this.supportersService.index(); 25 | return new ServerResponse(200, 'Successfully get supporters', data); 26 | } catch (err: any) { 27 | this.logger.error(`Failed to get supporters: ${err}`); 28 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 29 | } 30 | } 31 | 32 | @Post('') 33 | @UseGuards(AdminGuard) 34 | async store(@Body() body) { 35 | try { 36 | await this.supportersService.store(body); 37 | return new ServerResponse(200, 'Successfully created supporter'); 38 | } catch (err: any) { 39 | this.logger.error(`Failed to create supporter: ${err}`); 40 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/supporters/supporters.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { SupportersService } from './supporters.service'; 4 | import { SupportersController } from './supporters.controller'; 5 | import { PrismaModule } from '../prisma/prisma.module'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [SupportersService], 10 | controllers: [SupportersController], 11 | }) 12 | export class SupportersModule {} 13 | -------------------------------------------------------------------------------- /src/supporters/supporters.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { SupportersService } from './supporters.service'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | 6 | describe('SupportersService', () => { 7 | let service: SupportersService; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | providers: [SupportersService], 12 | }) 13 | .useMocker((token) => { 14 | if (token === PrismaService) { 15 | return { 16 | supplyCategory: { findMany: jest.fn().mockResolvedValue(0) }, 17 | }; 18 | } 19 | }) 20 | .compile(); 21 | 22 | service = module.get(SupportersService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/supporters/supporters.service.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { CreateSupporterSchema } from './types'; 6 | 7 | @Injectable() 8 | export class SupportersService { 9 | constructor(private readonly prismaService: PrismaService) {} 10 | 11 | async index() { 12 | return await this.prismaService.supporters.findMany({}); 13 | } 14 | 15 | async store(body: z.infer) { 16 | const payload = CreateSupporterSchema.parse(body); 17 | await this.prismaService.supporters.create({ 18 | data: { ...payload, createdAt: new Date().toISOString() }, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/supporters/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | const SupporterSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | imageUrl: z.string(), 7 | link: z.string(), 8 | createdAt: z.string(), 9 | updatedAt: z.string().nullable().optional(), 10 | }); 11 | 12 | const CreateSupporterSchema = SupporterSchema.omit({ 13 | id: true, 14 | createdAt: true, 15 | updatedAt: true, 16 | }); 17 | 18 | export { SupporterSchema, CreateSupporterSchema }; 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | const SearchSchema = z.object({ 4 | perPage: z.preprocess( 5 | (v) => +((v ?? '20') as string), 6 | z.number().min(1).max(100), 7 | ), 8 | page: z.preprocess((v) => +((v ?? '1') as string), z.number().min(1)), 9 | search: z.string().default(''), 10 | order: z.enum(['desc', 'asc']).default('desc'), 11 | orderBy: z.string().default('createdAt'), 12 | }); 13 | 14 | export { SearchSchema }; 15 | -------------------------------------------------------------------------------- /src/users/types.ts: -------------------------------------------------------------------------------- 1 | import { AccessLevel } from '@prisma/client'; 2 | import z from 'zod'; 3 | 4 | import { removeNotNumbers } from '../utils'; 5 | 6 | const UserSchema = z.object({ 7 | id: z.string().uuid(), 8 | name: z.string(), 9 | lastName: z.string(), 10 | login: z.string().transform((v) => v.toLowerCase()), 11 | password: z.string(), 12 | phone: z.string().transform(removeNotNumbers), 13 | accessLevel: z.nativeEnum(AccessLevel), 14 | createdAt: z.string(), 15 | updatedAt: z.string().nullable().optional(), 16 | }); 17 | 18 | const CreateUserSchema = UserSchema.pick({ 19 | name: true, 20 | lastName: true, 21 | phone: true, 22 | }); 23 | 24 | const UpdateUserSchema = UserSchema.omit({ 25 | id: true, 26 | accessLevel: true, 27 | createdAt: true, 28 | updatedAt: true, 29 | }).partial(); 30 | 31 | export { CreateUserSchema, UpdateUserSchema }; 32 | -------------------------------------------------------------------------------- /src/users/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersController } from './users.controller'; 3 | import { UsersService } from './users.service'; 4 | import { PrismaService } from 'src/prisma/prisma.service'; 5 | 6 | describe('UsersController', () => { 7 | let controller: UsersController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [UsersController], 12 | providers: [UsersService], 13 | }) 14 | .useMocker((token) => { 15 | if (token === PrismaService) { 16 | return {}; 17 | } 18 | }) 19 | .compile(); 20 | 21 | controller = module.get(UsersController); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(controller).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Logger, 7 | Param, 8 | Post, 9 | Put, 10 | Req, 11 | UseGuards, 12 | } from '@nestjs/common'; 13 | import { ApiTags, ApiBody, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; 14 | 15 | import { UserGuard } from '@/guards/user.guard'; 16 | import { ServerResponse } from '../utils'; 17 | import { UsersService } from './users.service'; 18 | import { AdminGuard } from '@/guards/admin.guard'; 19 | 20 | @ApiTags('Usuários') 21 | @Controller('users') 22 | export class UsersController { 23 | private logger = new Logger(UsersController.name); 24 | 25 | constructor(private readonly userServices: UsersService) {} 26 | 27 | @Post('') 28 | @ApiBearerAuth() 29 | @ApiOperation({ 30 | summary: 'Cria um novo usuário', 31 | description: 'Esta rota é usada para criar um novo usuário no sistema.', 32 | }) 33 | @ApiBody({ 34 | schema: { 35 | type: 'object', 36 | properties: { 37 | name: { type: 'string' }, 38 | lastName: { type: 'string' }, 39 | phone: { type: 'string' }, 40 | }, 41 | required: ['name', 'lastName', 'phone'], 42 | }, 43 | examples: { 44 | 'Exemplo 1': { 45 | value: { 46 | name: 'Administrador', 47 | lastName: 'Web', 48 | phone: '(31) 999999999', 49 | }, 50 | }, 51 | }, 52 | }) 53 | async store(@Body() body) { 54 | try { 55 | await this.userServices.store(body); 56 | return new ServerResponse(201, 'Successfully created user'); 57 | } catch (err: any) { 58 | this.logger.error(`Failed to store user: ${err}`); 59 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 60 | } 61 | } 62 | 63 | @Put(':id') 64 | @UseGuards(AdminGuard) 65 | @ApiBearerAuth() 66 | @ApiOperation({ 67 | summary: 'Atualiza um usuário pelo ID', 68 | description: 69 | 'Esta rota é usada para atualizar um usuário específico no sistema, podendo ser informado um ou mais campos.', 70 | }) 71 | @ApiBody({ 72 | schema: { 73 | type: 'object', 74 | properties: { 75 | name: { type: 'string' }, 76 | lastName: { type: 'string' }, 77 | phone: { type: 'string' }, 78 | login: { type: 'string' }, 79 | password: { type: 'string' }, 80 | }, 81 | required: [], 82 | }, 83 | examples: { 84 | 'Exemplo 1': { 85 | value: { 86 | name: 'Administrador', 87 | lastName: 'Web', 88 | phone: '(31) 999999999', 89 | login: 'admin', 90 | password: '123456', 91 | }, 92 | }, 93 | }, 94 | }) 95 | async update(@Body() body, @Param('id') id: string) { 96 | try { 97 | await this.userServices.update(id, body); 98 | return new ServerResponse(201, 'Successfully updated user'); 99 | } catch (err: any) { 100 | this.logger.error(`Failed to update user: ${err}`); 101 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 102 | } 103 | } 104 | 105 | @Put('') 106 | @UseGuards(UserGuard) 107 | @ApiBearerAuth() 108 | @ApiOperation({ 109 | summary: 'Atualiza o seu próprio usuário', 110 | description: 111 | 'Esta rota é usada para atualizar o próprio usuário no sistema, podendo ser informado um ou mais campos.', 112 | }) 113 | @ApiBody({ 114 | schema: { 115 | type: 'object', 116 | properties: { 117 | name: { type: 'string' }, 118 | lastName: { type: 'string' }, 119 | phone: { type: 'string' }, 120 | login: { type: 'string' }, 121 | password: { type: 'string' }, 122 | }, 123 | required: [], 124 | }, 125 | examples: { 126 | 'Exemplo 1': { 127 | value: { 128 | name: 'João', 129 | lastName: 'Neves', 130 | phone: '(11) 99999-9999', 131 | login: 'joaodasneves', 132 | password: '12345678', 133 | }, 134 | }, 135 | }, 136 | }) 137 | async selfUpdate(@Body() body, @Req() req) { 138 | try { 139 | const { userId } = req.user; 140 | await this.userServices.update(userId, body); 141 | return new ServerResponse(201, 'Successfully updated'); 142 | } catch (err: any) { 143 | this.logger.error(`Failed to update user: ${err}`); 144 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 145 | } 146 | } 147 | 148 | @Get('find/:field/:value') 149 | async find(@Param('field') field: string, @Param('value') value: string) { 150 | try { 151 | const result = await this.userServices.checkIfUserExists({ 152 | [field]: value, 153 | }); 154 | return new ServerResponse(201, 'Successfully searched user', { 155 | exists: result, 156 | }); 157 | } catch (err: any) { 158 | this.logger.error(`Failed to find user: ${err}`); 159 | throw new HttpException(err?.code ?? err?.name ?? `${err}`, 400); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PrismaModule } from '../prisma/prisma.module'; 4 | import { UsersController } from './users.controller'; 5 | import { UsersService } from './users.service'; 6 | 7 | @Module({ 8 | imports: [PrismaModule], 9 | providers: [UsersService], 10 | controllers: [UsersController], 11 | }) 12 | export class UsersModule {} 13 | -------------------------------------------------------------------------------- /src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersService } from './users.service'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | describe('UsersService', () => { 6 | let service: UsersService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [UsersService], 11 | }) 12 | .useMocker((token) => { 13 | if (token === PrismaService) { 14 | return {}; 15 | } 16 | }) 17 | .compile(); 18 | 19 | service = module.get(UsersService); 20 | }); 21 | 22 | it('should be defined', () => { 23 | expect(service).toBeDefined(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | import { CreateUserSchema, UpdateUserSchema } from './types'; 5 | import { User } from '@prisma/client'; 6 | 7 | @Injectable() 8 | export class UsersService { 9 | constructor(private readonly prismaService: PrismaService) {} 10 | 11 | async store(body: any) { 12 | const { name, lastName, phone } = CreateUserSchema.parse(body); 13 | await this.prismaService.user.create({ 14 | data: { 15 | name, 16 | lastName, 17 | phone, 18 | password: phone, 19 | login: phone, 20 | createdAt: new Date().toISOString(), 21 | }, 22 | }); 23 | } 24 | 25 | async update(id: string, body: any) { 26 | const payload = UpdateUserSchema.parse(body); 27 | await this.prismaService.user.update({ 28 | where: { 29 | id, 30 | }, 31 | data: { 32 | ...payload, 33 | updatedAt: new Date().toISOString(), 34 | }, 35 | }); 36 | } 37 | 38 | async checkIfUserExists(payload: Partial): Promise { 39 | const result = await this.prismaService.user.findFirst({ 40 | where: payload, 41 | }); 42 | 43 | return !!result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ServerResponse, 3 | removeNotNumbers, 4 | getSessionData, 5 | deepMerge, 6 | capitalize, 7 | } from './utils'; 8 | 9 | export { 10 | capitalize, 11 | ServerResponse, 12 | removeNotNumbers, 13 | getSessionData, 14 | deepMerge, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { GeolocationFilter } from 'src/shelter/types/search.types'; 3 | 4 | class ServerResponse { 5 | readonly message: string; 6 | private readonly statusCode: number; 7 | private readonly respData?: T; 8 | 9 | constructor(statusCode: number, message: string, data?: T) { 10 | this.statusCode = statusCode; 11 | this.message = message; 12 | this.respData = data; 13 | } 14 | 15 | public get data(): { statusCode: number; message: string; data?: T } { 16 | const resp = { 17 | statusCode: this.statusCode, 18 | message: this.message, 19 | data: this.respData, 20 | }; 21 | if (!resp?.data) delete resp.data; 22 | return resp; 23 | } 24 | } 25 | 26 | function removeNotNumbers(input: string): string { 27 | return input.replace(/[^0-9]/g, ''); 28 | } 29 | 30 | function capitalize(input: string): string { 31 | return input 32 | .trim() 33 | .toLowerCase() 34 | .split(' ') 35 | .map((t) => t[0].toUpperCase() + t.slice(1)) 36 | .join(' '); 37 | } 38 | 39 | function getSessionData(token?: string): { userId: string; sessionId: string } { 40 | try { 41 | if (token) { 42 | const splited = token.split('.'); 43 | if (splited.length === 3) { 44 | const { userId, sessionId } = JSON.parse( 45 | Buffer.from(splited[1], 'base64').toString('utf-8'), 46 | ); 47 | return { userId, sessionId }; 48 | } 49 | } 50 | } catch (err) { 51 | Logger.error(`Error to get session data: ${err}`, getSessionData.name); 52 | } 53 | return { userId: '', sessionId: '' }; 54 | } 55 | 56 | function deepMerge(target: Record, source: Record) { 57 | if (Array.isArray(target) && Array.isArray(source)) { 58 | return [...target, ...source]; 59 | } else if ( 60 | typeof target === 'object' && 61 | target !== null && 62 | typeof source === 'object' && 63 | source !== null 64 | ) { 65 | const merged = { ...target }; 66 | for (const key in source) { 67 | if (source.hasOwnProperty(key)) { 68 | merged[key] = target.hasOwnProperty(key) 69 | ? deepMerge(target[key], source[key]) 70 | : source[key]; 71 | } 72 | } 73 | return merged; 74 | } else { 75 | return source; 76 | } 77 | } 78 | 79 | interface Coordinates { 80 | maxLat: number; 81 | minLat: number; 82 | maxLong: number; 83 | minLong: number; 84 | } 85 | 86 | function calculateGeolocationBounds({ 87 | latitude, 88 | longitude, 89 | radiusInMeters, 90 | }: GeolocationFilter): Coordinates { 91 | const earthRadius = 6371000; 92 | 93 | const latRad = (latitude * Math.PI) / 180; 94 | 95 | const radiusRad = radiusInMeters / earthRadius; 96 | 97 | const maxLat = latitude + radiusRad * (180 / Math.PI); 98 | const minLat = latitude - radiusRad * (180 / Math.PI); 99 | 100 | const deltaLong = Math.asin(Math.sin(radiusRad) / Math.cos(latRad)); 101 | 102 | const maxLong = longitude + deltaLong * (180 / Math.PI); 103 | const minLong = longitude - deltaLong * (180 / Math.PI); 104 | 105 | return { 106 | maxLat, 107 | minLat, 108 | maxLong, 109 | minLong, 110 | }; 111 | } 112 | 113 | function removeEmptyStrings(obj): T { 114 | return Object.entries(obj).reduce( 115 | (prev, [key, value]) => (value === '' ? prev : { ...prev, [key]: value }), 116 | {}, 117 | ) as T; 118 | } 119 | 120 | export { 121 | ServerResponse, 122 | calculateGeolocationBounds, 123 | capitalize, 124 | deepMerge, 125 | getSessionData, 126 | removeNotNumbers, 127 | removeEmptyStrings, 128 | }; 129 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from 'src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(404); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/jest.e2e.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | import { default as projectConfig } from '../jest.config'; 3 | 4 | const config: Config = { 5 | ...projectConfig, 6 | rootDir: '.', 7 | moduleNameMapper: { 8 | '^src/(.*)$': '/../src/$1', 9 | '^@/(.*)$': '/../src/$1', 10 | '^test/(.*)$': '/$1', 11 | }, 12 | testRegex: '.e2e-spec.ts$', 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "noImplicitAny": false, 16 | "strictBindCallApply": false, 17 | "forceConsistentCasingInFileNames": false, 18 | "noFallthroughCasesInSwitch": false, 19 | "strictNullChecks": true, 20 | "strict": true, 21 | "paths": { 22 | "@/decorators/*": ["./src/decorators/*"], 23 | "@/decorators": ["./src/decorators"], 24 | "@/interceptors/*": ["./src/interceptors/*"], 25 | "@/interceptors": ["./src/interceptors"], 26 | "@/middlewares/*": ["./src/middlewares/*"], 27 | "@/middlewares": ["./src/middlewares"], 28 | "@/utils/*": ["./src/utils/*"], 29 | "@/utils": ["./src/utils"], 30 | "@/guards/*": ["./src/guards/*"], 31 | "@/guards": ["./src/guards"] 32 | } 33 | }, 34 | "watchOptions": { 35 | "watchFile": "fixedPollingInterval" 36 | } 37 | } 38 | --------------------------------------------------------------------------------