├── .github └── workflows │ └── run_tests.yaml ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── seed.js └── users.js ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src ├── config.js ├── db.js └── index.js └── test └── api.test.js /.github/workflows/run_tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.js' 7 | - '**.json' 8 | - '**.yaml' 9 | branches: 10 | - main 11 | concurrency: 12 | group: staging_environment 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | setup-and-test-app: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | - name: Install Docker Compose 22 | run: | 23 | sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 24 | sudo chmod +x /usr/local/bin/docker-compose 25 | docker-compose --version 26 | 27 | - name: Start Postgres and Adminer 28 | run: docker-compose up -d mongodb 29 | 30 | - name: Use Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | 35 | - name: Restore Dependencies for web-api 36 | run: npm ci 37 | 38 | - name: Run Tests for app/web-api 39 | run: npm test -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Test Debugger", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ 10 | "run", 11 | "test:debug" 12 | ], 13 | "skipFiles": [ 14 | "/**/**" 15 | ], 16 | "console": "integratedTerminal" 17 | }, 18 | ], 19 | "compounds": [] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "window.zoomLevel": 1 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.11-alpine3.18 as build 2 | 3 | WORKDIR /src/ 4 | 5 | COPY package.json package-lock.json /src/ 6 | 7 | COPY . /src/ 8 | 9 | RUN npm ci --silent 10 | 11 | USER node 12 | 13 | CMD npm run start 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2024 Erick Wendel 2 | 3 | Permission is hereby granted, free 4 | of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CRUD Ops with Node.js, Fastify, and MongoDB using the Node.js Test Runner 2 | 3 | ![Build Status](https://github.com/ErickWendel/nodejs-fastify-mongodb-crud/workflows/Run%20tests/badge.svg) 4 | 5 | ## Description 6 | 7 | This project demonstrates how to perform CRUD operations using Node.js with the Fastify framework and MongoDB. It includes unit tests that verify the functionality of the API endpoints and track code coverage. 8 | 9 | ## Getting Started 10 | 11 | ### Prerequisites 12 | - Docker and Docker compose 13 | - Node.js (version 20 or later) 14 | - MongoDB (local or cloud instance) 15 | 16 | ### Installation 17 | 18 | 1. Clone the repository: 19 | ```bash 20 | git clone https://github.com/ErickWendel/nodejs-fastify-mongodb-crud.git 21 | cd nodejs-fastify-mongodb-crud 22 | ``` 23 | 24 | 2. Install the dependencies: 25 | ```bash 26 | npm ci 27 | ``` 28 | ### Running Tests 29 | 30 | To run the tests and see code coverage, use: 31 | ```bash 32 | docker-compose up -d mongodb 33 | npm test 34 | ``` 35 | This will execute the tests defined in the project and provide a coverage report. 36 | 37 | 38 | ### Running the Project 39 | 40 | To initialize the MongoDB, run: 41 | ```bash 42 | docker-compose up -d mongodb 43 | ``` 44 | 45 | To initialize the project, run: 46 | ```bash 47 | npm start 48 | ``` 49 | The server will start, and you can access the API on `http://localhost:9999` (or your specified port). 50 | 51 | 52 | ### API Endpoints 53 | 54 | Here are the available routes for performing CRUD operations on customers: 55 | 56 | 1. **Create a Customer** (POST) 57 | ```bash 58 | curl -X POST http://localhost:9999/v1/customers \ 59 | -H "Content-Type: application/json" \ 60 | -d '{"name": "John Doe", "phone": "123456789"}' 61 | ``` 62 | 63 | 2. **Retrieve All Customers** (GET) 64 | ```bash 65 | curl http://localhost:9999/v1/customers 66 | ``` 67 | 68 | 3. **Retrieve a Customer by ID** (GET) 69 | ```bash 70 | curl http://localhost:9999/v1/customers/ 71 | ``` 72 | 73 | 4. **Update a Customer** (PUT) 74 | ```bash 75 | curl -X PUT http://localhost:9999/v1/customers/ \ 76 | -H "Content-Type: application/json" \ 77 | -d '{"name": "Jane Doe", "phone": "987654321"}' 78 | ``` 79 | 80 | 5. **Delete a Customer** (DELETE) 81 | ```bash 82 | curl -X DELETE http://localhost:9999/v1/customers/ 83 | ``` 84 | 85 | ## License 86 | 87 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 88 | 89 | ## Acknowledgments 90 | 91 | - [Fastify](https://www.fastify.io/) - Fast and low-overhead web framework for Node.js 92 | - [MongoDB](https://www.mongodb.com/) - NoSQL database for storing data 93 | - [Node.js Test Runner](https://nodejs.org/en/docs/guides/test-runner/) - Built-in test runner for Node.js 94 | -------------------------------------------------------------------------------- /config/seed.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb' 2 | import config from './../src/config.js'; 3 | import { users } from './users.js'; 4 | const isTestEnv = process.env.NODE_ENV === 'test' 5 | const log = (...args) => { 6 | if (isTestEnv) return; 7 | console.log(...args) 8 | } 9 | 10 | async function runSeed() { 11 | 12 | const client = new MongoClient(config.dbURL); 13 | try { 14 | await client.connect(); 15 | 16 | log(`Db connected successfully to ${config.dbName}!`); 17 | // create index for id 18 | const db = client.db(config.dbName); 19 | 20 | const collection = db.collection(config.collection); 21 | 22 | 23 | await collection.deleteMany({}) 24 | await Promise.all(users.map(i => collection.insertOne({ ...i }))) 25 | 26 | log(await collection.find().toArray()) 27 | 28 | } catch (err) { 29 | log(err.stack); 30 | } finally { 31 | await client.close(); 32 | } 33 | } 34 | 35 | if (!isTestEnv) runSeed(); 36 | 37 | export { runSeed } -------------------------------------------------------------------------------- /config/users.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | name: 'John Doe', 4 | phone: '123456789', 5 | }, 6 | { 7 | name: 'Jane Doe', 8 | phone: '987654321', 9 | }, 10 | { 11 | name: 'Chapolin Colorado', 12 | phone: '123456789', 13 | } 14 | ] 15 | export { users } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | api01-test: 5 | hostname: api01 6 | build: . 7 | environment: 8 | DB_HOST: mongodb 9 | DB_NAME: customers 10 | ports: 11 | - "9999:9999" 12 | # command: npm run test 13 | # command: npm run test:dev 14 | command: npm run start 15 | volumes: 16 | - .:/src 17 | - nodemodules:/src/node_modules 18 | depends_on: 19 | - mongodb 20 | 21 | mongodb: 22 | image: mongo 23 | ports: 24 | - 27017:27017 25 | environment: 26 | MONGO_INITDB_ROOT_USERNAME: root 27 | MONGO_INITDB_ROOT_PASSWORD: example 28 | 29 | volumes: 30 | nodemodules: {} 31 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testes-com-bancos-de-dados", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "testes-com-bancos-de-dados", 9 | "version": "0.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "fastify": "^4.26.1", 13 | "mongodb": "^6.3.0" 14 | }, 15 | "engines": { 16 | "node": "v20.11.0" 17 | } 18 | }, 19 | "node_modules/@fastify/ajv-compiler": { 20 | "version": "3.5.0", 21 | "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", 22 | "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", 23 | "dependencies": { 24 | "ajv": "^8.11.0", 25 | "ajv-formats": "^2.1.1", 26 | "fast-uri": "^2.0.0" 27 | } 28 | }, 29 | "node_modules/@fastify/error": { 30 | "version": "3.4.1", 31 | "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", 32 | "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" 33 | }, 34 | "node_modules/@fastify/fast-json-stringify-compiler": { 35 | "version": "4.3.0", 36 | "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", 37 | "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", 38 | "dependencies": { 39 | "fast-json-stringify": "^5.7.0" 40 | } 41 | }, 42 | "node_modules/@fastify/merge-json-schemas": { 43 | "version": "0.1.1", 44 | "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", 45 | "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", 46 | "dependencies": { 47 | "fast-deep-equal": "^3.1.3" 48 | } 49 | }, 50 | "node_modules/@mongodb-js/saslprep": { 51 | "version": "1.1.4", 52 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", 53 | "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", 54 | "dependencies": { 55 | "sparse-bitfield": "^3.0.3" 56 | } 57 | }, 58 | "node_modules/@types/webidl-conversions": { 59 | "version": "7.0.3", 60 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 61 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" 62 | }, 63 | "node_modules/@types/whatwg-url": { 64 | "version": "11.0.4", 65 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", 66 | "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", 67 | "dependencies": { 68 | "@types/webidl-conversions": "*" 69 | } 70 | }, 71 | "node_modules/abort-controller": { 72 | "version": "3.0.0", 73 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 74 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 75 | "dependencies": { 76 | "event-target-shim": "^5.0.0" 77 | }, 78 | "engines": { 79 | "node": ">=6.5" 80 | } 81 | }, 82 | "node_modules/abstract-logging": { 83 | "version": "2.0.1", 84 | "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", 85 | "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" 86 | }, 87 | "node_modules/ajv": { 88 | "version": "8.12.0", 89 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", 90 | "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", 91 | "dependencies": { 92 | "fast-deep-equal": "^3.1.1", 93 | "json-schema-traverse": "^1.0.0", 94 | "require-from-string": "^2.0.2", 95 | "uri-js": "^4.2.2" 96 | }, 97 | "funding": { 98 | "type": "github", 99 | "url": "https://github.com/sponsors/epoberezkin" 100 | } 101 | }, 102 | "node_modules/ajv-formats": { 103 | "version": "2.1.1", 104 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", 105 | "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", 106 | "dependencies": { 107 | "ajv": "^8.0.0" 108 | }, 109 | "peerDependencies": { 110 | "ajv": "^8.0.0" 111 | }, 112 | "peerDependenciesMeta": { 113 | "ajv": { 114 | "optional": true 115 | } 116 | } 117 | }, 118 | "node_modules/archy": { 119 | "version": "1.0.0", 120 | "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", 121 | "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" 122 | }, 123 | "node_modules/atomic-sleep": { 124 | "version": "1.0.0", 125 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 126 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 127 | "engines": { 128 | "node": ">=8.0.0" 129 | } 130 | }, 131 | "node_modules/avvio": { 132 | "version": "8.3.0", 133 | "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", 134 | "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", 135 | "dependencies": { 136 | "@fastify/error": "^3.3.0", 137 | "archy": "^1.0.0", 138 | "debug": "^4.0.0", 139 | "fastq": "^1.17.1" 140 | } 141 | }, 142 | "node_modules/base64-js": { 143 | "version": "1.5.1", 144 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 145 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 146 | "funding": [ 147 | { 148 | "type": "github", 149 | "url": "https://github.com/sponsors/feross" 150 | }, 151 | { 152 | "type": "patreon", 153 | "url": "https://www.patreon.com/feross" 154 | }, 155 | { 156 | "type": "consulting", 157 | "url": "https://feross.org/support" 158 | } 159 | ] 160 | }, 161 | "node_modules/bson": { 162 | "version": "6.3.0", 163 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", 164 | "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", 165 | "engines": { 166 | "node": ">=16.20.1" 167 | } 168 | }, 169 | "node_modules/buffer": { 170 | "version": "6.0.3", 171 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 172 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 173 | "funding": [ 174 | { 175 | "type": "github", 176 | "url": "https://github.com/sponsors/feross" 177 | }, 178 | { 179 | "type": "patreon", 180 | "url": "https://www.patreon.com/feross" 181 | }, 182 | { 183 | "type": "consulting", 184 | "url": "https://feross.org/support" 185 | } 186 | ], 187 | "dependencies": { 188 | "base64-js": "^1.3.1", 189 | "ieee754": "^1.2.1" 190 | } 191 | }, 192 | "node_modules/cookie": { 193 | "version": "0.6.0", 194 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 195 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 196 | "engines": { 197 | "node": ">= 0.6" 198 | } 199 | }, 200 | "node_modules/debug": { 201 | "version": "4.3.4", 202 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 203 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 204 | "dependencies": { 205 | "ms": "2.1.2" 206 | }, 207 | "engines": { 208 | "node": ">=6.0" 209 | }, 210 | "peerDependenciesMeta": { 211 | "supports-color": { 212 | "optional": true 213 | } 214 | } 215 | }, 216 | "node_modules/event-target-shim": { 217 | "version": "5.0.1", 218 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 219 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 220 | "engines": { 221 | "node": ">=6" 222 | } 223 | }, 224 | "node_modules/events": { 225 | "version": "3.3.0", 226 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 227 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 228 | "engines": { 229 | "node": ">=0.8.x" 230 | } 231 | }, 232 | "node_modules/fast-content-type-parse": { 233 | "version": "1.1.0", 234 | "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", 235 | "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" 236 | }, 237 | "node_modules/fast-decode-uri-component": { 238 | "version": "1.0.1", 239 | "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", 240 | "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" 241 | }, 242 | "node_modules/fast-deep-equal": { 243 | "version": "3.1.3", 244 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 245 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 246 | }, 247 | "node_modules/fast-json-stringify": { 248 | "version": "5.12.0", 249 | "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", 250 | "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", 251 | "dependencies": { 252 | "@fastify/merge-json-schemas": "^0.1.0", 253 | "ajv": "^8.10.0", 254 | "ajv-formats": "^2.1.1", 255 | "fast-deep-equal": "^3.1.3", 256 | "fast-uri": "^2.1.0", 257 | "json-schema-ref-resolver": "^1.0.1", 258 | "rfdc": "^1.2.0" 259 | } 260 | }, 261 | "node_modules/fast-querystring": { 262 | "version": "1.1.2", 263 | "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", 264 | "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", 265 | "dependencies": { 266 | "fast-decode-uri-component": "^1.0.1" 267 | } 268 | }, 269 | "node_modules/fast-redact": { 270 | "version": "3.3.0", 271 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", 272 | "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", 273 | "engines": { 274 | "node": ">=6" 275 | } 276 | }, 277 | "node_modules/fast-uri": { 278 | "version": "2.3.0", 279 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", 280 | "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" 281 | }, 282 | "node_modules/fastify": { 283 | "version": "4.26.1", 284 | "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz", 285 | "integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==", 286 | "funding": [ 287 | { 288 | "type": "github", 289 | "url": "https://github.com/sponsors/fastify" 290 | }, 291 | { 292 | "type": "opencollective", 293 | "url": "https://opencollective.com/fastify" 294 | } 295 | ], 296 | "dependencies": { 297 | "@fastify/ajv-compiler": "^3.5.0", 298 | "@fastify/error": "^3.4.0", 299 | "@fastify/fast-json-stringify-compiler": "^4.3.0", 300 | "abstract-logging": "^2.0.1", 301 | "avvio": "^8.3.0", 302 | "fast-content-type-parse": "^1.1.0", 303 | "fast-json-stringify": "^5.8.0", 304 | "find-my-way": "^8.0.0", 305 | "light-my-request": "^5.11.0", 306 | "pino": "^8.17.0", 307 | "process-warning": "^3.0.0", 308 | "proxy-addr": "^2.0.7", 309 | "rfdc": "^1.3.0", 310 | "secure-json-parse": "^2.7.0", 311 | "semver": "^7.5.4", 312 | "toad-cache": "^3.3.0" 313 | } 314 | }, 315 | "node_modules/fastq": { 316 | "version": "1.17.1", 317 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 318 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 319 | "dependencies": { 320 | "reusify": "^1.0.4" 321 | } 322 | }, 323 | "node_modules/find-my-way": { 324 | "version": "8.1.0", 325 | "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", 326 | "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", 327 | "dependencies": { 328 | "fast-deep-equal": "^3.1.3", 329 | "fast-querystring": "^1.0.0", 330 | "safe-regex2": "^2.0.0" 331 | }, 332 | "engines": { 333 | "node": ">=14" 334 | } 335 | }, 336 | "node_modules/forwarded": { 337 | "version": "0.2.0", 338 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 339 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 340 | "engines": { 341 | "node": ">= 0.6" 342 | } 343 | }, 344 | "node_modules/ieee754": { 345 | "version": "1.2.1", 346 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 347 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 348 | "funding": [ 349 | { 350 | "type": "github", 351 | "url": "https://github.com/sponsors/feross" 352 | }, 353 | { 354 | "type": "patreon", 355 | "url": "https://www.patreon.com/feross" 356 | }, 357 | { 358 | "type": "consulting", 359 | "url": "https://feross.org/support" 360 | } 361 | ] 362 | }, 363 | "node_modules/ipaddr.js": { 364 | "version": "1.9.1", 365 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 366 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 367 | "engines": { 368 | "node": ">= 0.10" 369 | } 370 | }, 371 | "node_modules/json-schema-ref-resolver": { 372 | "version": "1.0.1", 373 | "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", 374 | "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", 375 | "dependencies": { 376 | "fast-deep-equal": "^3.1.3" 377 | } 378 | }, 379 | "node_modules/json-schema-traverse": { 380 | "version": "1.0.0", 381 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 382 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" 383 | }, 384 | "node_modules/light-my-request": { 385 | "version": "5.11.1", 386 | "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.1.tgz", 387 | "integrity": "sha512-KXAh2m6VRlkWCk2KfmHE7tLBXKh30JE0tXUJY4dNxje4oLmPKUqlUfImiEQZLphx+Z9KTQcVv4DjGnJxkVOIbA==", 388 | "dependencies": { 389 | "cookie": "^0.6.0", 390 | "process-warning": "^2.0.0", 391 | "set-cookie-parser": "^2.4.1" 392 | } 393 | }, 394 | "node_modules/light-my-request/node_modules/process-warning": { 395 | "version": "2.3.2", 396 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", 397 | "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" 398 | }, 399 | "node_modules/lru-cache": { 400 | "version": "6.0.0", 401 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 402 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 403 | "dependencies": { 404 | "yallist": "^4.0.0" 405 | }, 406 | "engines": { 407 | "node": ">=10" 408 | } 409 | }, 410 | "node_modules/memory-pager": { 411 | "version": "1.5.0", 412 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 413 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" 414 | }, 415 | "node_modules/mongodb": { 416 | "version": "6.3.0", 417 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", 418 | "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", 419 | "dependencies": { 420 | "@mongodb-js/saslprep": "^1.1.0", 421 | "bson": "^6.2.0", 422 | "mongodb-connection-string-url": "^3.0.0" 423 | }, 424 | "engines": { 425 | "node": ">=16.20.1" 426 | }, 427 | "peerDependencies": { 428 | "@aws-sdk/credential-providers": "^3.188.0", 429 | "@mongodb-js/zstd": "^1.1.0", 430 | "gcp-metadata": "^5.2.0", 431 | "kerberos": "^2.0.1", 432 | "mongodb-client-encryption": ">=6.0.0 <7", 433 | "snappy": "^7.2.2", 434 | "socks": "^2.7.1" 435 | }, 436 | "peerDependenciesMeta": { 437 | "@aws-sdk/credential-providers": { 438 | "optional": true 439 | }, 440 | "@mongodb-js/zstd": { 441 | "optional": true 442 | }, 443 | "gcp-metadata": { 444 | "optional": true 445 | }, 446 | "kerberos": { 447 | "optional": true 448 | }, 449 | "mongodb-client-encryption": { 450 | "optional": true 451 | }, 452 | "snappy": { 453 | "optional": true 454 | }, 455 | "socks": { 456 | "optional": true 457 | } 458 | } 459 | }, 460 | "node_modules/mongodb-connection-string-url": { 461 | "version": "3.0.0", 462 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", 463 | "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", 464 | "dependencies": { 465 | "@types/whatwg-url": "^11.0.2", 466 | "whatwg-url": "^13.0.0" 467 | } 468 | }, 469 | "node_modules/ms": { 470 | "version": "2.1.2", 471 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 472 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 473 | }, 474 | "node_modules/on-exit-leak-free": { 475 | "version": "2.1.2", 476 | "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 477 | "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 478 | "engines": { 479 | "node": ">=14.0.0" 480 | } 481 | }, 482 | "node_modules/pino": { 483 | "version": "8.19.0", 484 | "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", 485 | "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", 486 | "dependencies": { 487 | "atomic-sleep": "^1.0.0", 488 | "fast-redact": "^3.1.1", 489 | "on-exit-leak-free": "^2.1.0", 490 | "pino-abstract-transport": "v1.1.0", 491 | "pino-std-serializers": "^6.0.0", 492 | "process-warning": "^3.0.0", 493 | "quick-format-unescaped": "^4.0.3", 494 | "real-require": "^0.2.0", 495 | "safe-stable-stringify": "^2.3.1", 496 | "sonic-boom": "^3.7.0", 497 | "thread-stream": "^2.0.0" 498 | }, 499 | "bin": { 500 | "pino": "bin.js" 501 | } 502 | }, 503 | "node_modules/pino-abstract-transport": { 504 | "version": "1.1.0", 505 | "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", 506 | "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", 507 | "dependencies": { 508 | "readable-stream": "^4.0.0", 509 | "split2": "^4.0.0" 510 | } 511 | }, 512 | "node_modules/pino-std-serializers": { 513 | "version": "6.2.2", 514 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", 515 | "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" 516 | }, 517 | "node_modules/process": { 518 | "version": "0.11.10", 519 | "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 520 | "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 521 | "engines": { 522 | "node": ">= 0.6.0" 523 | } 524 | }, 525 | "node_modules/process-warning": { 526 | "version": "3.0.0", 527 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", 528 | "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" 529 | }, 530 | "node_modules/proxy-addr": { 531 | "version": "2.0.7", 532 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 533 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 534 | "dependencies": { 535 | "forwarded": "0.2.0", 536 | "ipaddr.js": "1.9.1" 537 | }, 538 | "engines": { 539 | "node": ">= 0.10" 540 | } 541 | }, 542 | "node_modules/punycode": { 543 | "version": "2.3.1", 544 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 545 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 546 | "engines": { 547 | "node": ">=6" 548 | } 549 | }, 550 | "node_modules/quick-format-unescaped": { 551 | "version": "4.0.4", 552 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 553 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 554 | }, 555 | "node_modules/readable-stream": { 556 | "version": "4.5.2", 557 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", 558 | "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", 559 | "dependencies": { 560 | "abort-controller": "^3.0.0", 561 | "buffer": "^6.0.3", 562 | "events": "^3.3.0", 563 | "process": "^0.11.10", 564 | "string_decoder": "^1.3.0" 565 | }, 566 | "engines": { 567 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 568 | } 569 | }, 570 | "node_modules/real-require": { 571 | "version": "0.2.0", 572 | "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 573 | "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 574 | "engines": { 575 | "node": ">= 12.13.0" 576 | } 577 | }, 578 | "node_modules/require-from-string": { 579 | "version": "2.0.2", 580 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 581 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 582 | "engines": { 583 | "node": ">=0.10.0" 584 | } 585 | }, 586 | "node_modules/ret": { 587 | "version": "0.2.2", 588 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", 589 | "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", 590 | "engines": { 591 | "node": ">=4" 592 | } 593 | }, 594 | "node_modules/reusify": { 595 | "version": "1.0.4", 596 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 597 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 598 | "engines": { 599 | "iojs": ">=1.0.0", 600 | "node": ">=0.10.0" 601 | } 602 | }, 603 | "node_modules/rfdc": { 604 | "version": "1.3.1", 605 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", 606 | "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" 607 | }, 608 | "node_modules/safe-buffer": { 609 | "version": "5.2.1", 610 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 611 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 612 | "funding": [ 613 | { 614 | "type": "github", 615 | "url": "https://github.com/sponsors/feross" 616 | }, 617 | { 618 | "type": "patreon", 619 | "url": "https://www.patreon.com/feross" 620 | }, 621 | { 622 | "type": "consulting", 623 | "url": "https://feross.org/support" 624 | } 625 | ] 626 | }, 627 | "node_modules/safe-regex2": { 628 | "version": "2.0.0", 629 | "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", 630 | "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", 631 | "dependencies": { 632 | "ret": "~0.2.0" 633 | } 634 | }, 635 | "node_modules/safe-stable-stringify": { 636 | "version": "2.4.3", 637 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", 638 | "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", 639 | "engines": { 640 | "node": ">=10" 641 | } 642 | }, 643 | "node_modules/secure-json-parse": { 644 | "version": "2.7.0", 645 | "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", 646 | "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" 647 | }, 648 | "node_modules/semver": { 649 | "version": "7.6.0", 650 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", 651 | "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", 652 | "dependencies": { 653 | "lru-cache": "^6.0.0" 654 | }, 655 | "bin": { 656 | "semver": "bin/semver.js" 657 | }, 658 | "engines": { 659 | "node": ">=10" 660 | } 661 | }, 662 | "node_modules/set-cookie-parser": { 663 | "version": "2.6.0", 664 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", 665 | "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" 666 | }, 667 | "node_modules/sonic-boom": { 668 | "version": "3.8.0", 669 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", 670 | "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", 671 | "dependencies": { 672 | "atomic-sleep": "^1.0.0" 673 | } 674 | }, 675 | "node_modules/sparse-bitfield": { 676 | "version": "3.0.3", 677 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 678 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 679 | "dependencies": { 680 | "memory-pager": "^1.0.2" 681 | } 682 | }, 683 | "node_modules/split2": { 684 | "version": "4.2.0", 685 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 686 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 687 | "engines": { 688 | "node": ">= 10.x" 689 | } 690 | }, 691 | "node_modules/string_decoder": { 692 | "version": "1.3.0", 693 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 694 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 695 | "dependencies": { 696 | "safe-buffer": "~5.2.0" 697 | } 698 | }, 699 | "node_modules/thread-stream": { 700 | "version": "2.4.1", 701 | "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", 702 | "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", 703 | "dependencies": { 704 | "real-require": "^0.2.0" 705 | } 706 | }, 707 | "node_modules/toad-cache": { 708 | "version": "3.7.0", 709 | "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", 710 | "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", 711 | "engines": { 712 | "node": ">=12" 713 | } 714 | }, 715 | "node_modules/tr46": { 716 | "version": "4.1.1", 717 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", 718 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", 719 | "dependencies": { 720 | "punycode": "^2.3.0" 721 | }, 722 | "engines": { 723 | "node": ">=14" 724 | } 725 | }, 726 | "node_modules/uri-js": { 727 | "version": "4.4.1", 728 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 729 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 730 | "dependencies": { 731 | "punycode": "^2.1.0" 732 | } 733 | }, 734 | "node_modules/webidl-conversions": { 735 | "version": "7.0.0", 736 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 737 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 738 | "engines": { 739 | "node": ">=12" 740 | } 741 | }, 742 | "node_modules/whatwg-url": { 743 | "version": "13.0.0", 744 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", 745 | "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", 746 | "dependencies": { 747 | "tr46": "^4.1.1", 748 | "webidl-conversions": "^7.0.0" 749 | }, 750 | "engines": { 751 | "node": ">=16" 752 | } 753 | }, 754 | "node_modules/yallist": { 755 | "version": "4.0.0", 756 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 757 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 758 | } 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testes-com-bancos-de-dados", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "DB_NAME=customers node src/index.js", 8 | "dev": "DB_NAME=customers node --watch src/index.js", 9 | "test": "NODE_ENV=test node --experimental-test-coverage --test test/ ", 10 | "test:dev": "NODE_ENV=test node --watch --test test/", 11 | "test:debug": "NODE_ENV=test node --inspect --test --watch test/" 12 | }, 13 | "keywords": [], 14 | "author": "erickwendel", 15 | "license": "MIT", 16 | "type": "module", 17 | "engines": { 18 | "node": "v20.11.0" 19 | }, 20 | "dependencies": { 21 | "fastify": "^4.26.1", 22 | "mongodb": "^6.3.0" 23 | } 24 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | const randomName = randomUUID().slice(0, 4) 4 | 5 | const dbUser = process.env.DB_USER || 'root' 6 | const dbPassword = process.env.DB_PASSWORD || 'example' 7 | const dbHost = process.env.DB_HOST || 'localhost' 8 | const dbPort = process.env.DB_PORT || '27017' 9 | const dbName = process.env.DB_NAME || `${randomName}-test` 10 | 11 | const config = { 12 | dbName, 13 | collection: 'customers', 14 | dbURL: `mongodb://${dbUser}:${dbPassword}@${dbHost}:${dbPort}` 15 | } 16 | 17 | export default config 18 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import config from './config.js'; 3 | 4 | async function connect() { 5 | const dbClient = new MongoClient(config.dbURL); 6 | 7 | const db = dbClient.db(config.dbName); 8 | const dbUsers = db.collection(config.collection); 9 | 10 | console.log('Connected to the database'); 11 | 12 | return { collections: { dbUsers }, dbClient }; 13 | 14 | } 15 | 16 | async function getDb() { 17 | // Initial connection attempt 18 | const { collections, dbClient } = await connect(); 19 | 20 | return { collections, dbClient }; 21 | } 22 | 23 | export { 24 | getDb 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import { getDb } from './db.js' 3 | import { ObjectId } from 'mongodb' 4 | 5 | const fastify = Fastify({}) 6 | const isTestEnv = process.env.NODE_ENV === 'test'; 7 | 8 | if (!isTestEnv && !process.env.DB_NAME) { 9 | console.error('[error*****]: please, pass DB_NAME env before running it!') 10 | process.exit(1) 11 | } 12 | 13 | const { dbClient, collections: { dbUsers } } = await getDb() 14 | 15 | fastify.get('/v1/health', async (request, reply) => { 16 | reply.code(200).send({app: 'customers', version: 'v1.0.1'}) 17 | }) 18 | 19 | fastify.get('/v1/customers', async (request, reply) => { 20 | const users = await dbUsers 21 | .find({}) 22 | .sort({ name: 1 }) 23 | .toArray() 24 | 25 | return reply.code(200).send(users) 26 | }) 27 | 28 | fastify.get('/v1/customers/:id', { 29 | schema: { 30 | response: { 31 | 200: { 32 | type: 'object', 33 | properties: { 34 | id: { type: 'string' }, 35 | name: { type: 'string' }, 36 | phone: { type: 'string' }, 37 | }, 38 | }, 39 | 404: { 40 | type: 'object', 41 | properties: { 42 | message: { type: 'string' }, 43 | id: { type: 'string' }, 44 | }, 45 | }, 46 | 400: { 47 | type: 'object', 48 | properties: { 49 | message: { type: 'string' }, 50 | id: { type: 'string' }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, async (request, reply) => { 56 | const { id } = request.params 57 | if (!ObjectId.isValid(id)) { 58 | return reply.code(400).send({ message: 'the id is invalid!', id }) 59 | } 60 | 61 | const user = await dbUsers.findOne({ _id: ObjectId.createFromHexString(id) }) // Assuming id is stored in MongoDB ObjectId format 62 | 63 | if (!user) { 64 | return reply.code(404).send({ error: 'User not found' }) 65 | } 66 | const { _id, ...remainingUserData } = user 67 | 68 | return reply.code(200).send({ 69 | ...remainingUserData, 70 | id, 71 | }) 72 | }) 73 | 74 | fastify.post('/v1/customers', { 75 | schema: { 76 | body: { 77 | type: 'object', 78 | required: ['name', 'phone'], 79 | properties: { 80 | name: { type: 'string' }, 81 | phone: { type: 'string' }, 82 | }, 83 | }, 84 | response: { 85 | 201: { 86 | type: 'object', 87 | properties: { 88 | message: { type: 'string' }, 89 | id: { type: 'string' }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, async (request, reply) => { 95 | const user = request.body 96 | const result = await dbUsers.insertOne(user) 97 | return reply.code(201).send({ message: `user ${user.name} created!`, id: result.insertedId.toString() }) 98 | }) 99 | 100 | fastify.put('/v1/customers/:id', { 101 | schema: { 102 | body: { 103 | type: 'object', 104 | required: ['name', 'phone'], 105 | properties: { 106 | name: { type: 'string' }, 107 | phone: { type: 'string' }, 108 | }, 109 | }, 110 | response: { 111 | 200: { 112 | type: 'object', 113 | properties: { 114 | message: { type: 'string' }, 115 | id: { type: 'string' }, 116 | }, 117 | }, 118 | 404: { 119 | type: 'object', 120 | properties: { 121 | message: { type: 'string' }, 122 | id: { type: 'string' }, 123 | }, 124 | }, 125 | 400: { 126 | type: 'object', 127 | properties: { 128 | message: { type: 'string' }, 129 | id: { type: 'string' }, 130 | }, 131 | }, 132 | }, 133 | }, 134 | }, async (request, reply) => { 135 | const { id } = request.params 136 | const user = request.body 137 | if (!ObjectId.isValid(id)) { 138 | return reply.code(400).send({ message: 'the id is invalid!', id }) 139 | } 140 | 141 | const result = await dbUsers.updateOne({ _id: ObjectId.createFromHexString(id) }, { $set: user }) 142 | 143 | if (!result.modifiedCount) { 144 | return reply.code(404).send({ message: 'User not found or no changes made', id }) 145 | } 146 | 147 | return reply.code(200).send({ message: `User ${id} updated!`, id }) 148 | }) 149 | 150 | fastify.delete('/v1/customers/:id', { 151 | schema: { 152 | response: { 153 | 200: { 154 | type: 'object', 155 | properties: { 156 | message: { type: 'string' }, 157 | id: { type: 'string' }, 158 | }, 159 | }, 160 | 404: { 161 | type: 'object', 162 | properties: { 163 | message: { type: 'string' }, 164 | id: { type: 'string' }, 165 | }, 166 | }, 167 | 400: { 168 | type: 'object', 169 | properties: { 170 | message: { type: 'string' }, 171 | id: { type: 'string' }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, async (request, reply) => { 177 | const { id } = request.params 178 | if (!ObjectId.isValid(id)) { 179 | return reply.code(400).send({ message: 'the id is invalid!', id }) 180 | } 181 | 182 | const result = await dbUsers.deleteOne({ _id: ObjectId.createFromHexString(id) }) 183 | 184 | if (!result.deletedCount) { 185 | return reply.code(404) 186 | } 187 | 188 | return reply.code(200).send({ message: `User ${id} deleted!`, id }) 189 | }) 190 | 191 | fastify.addHook('onClose', async () => { 192 | console.log('server closed!') 193 | return dbClient.close() 194 | }) 195 | fastify.addHook('preHandler', (req, res, done) => { 196 | res.header("Access-Control-Allow-Origin", "*"); 197 | res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); 198 | res.header("Access-Control-Allow-Headers", "*"); 199 | 200 | const isPreflight = /options/i.test(req.method); 201 | if (isPreflight) { 202 | return res.send(); 203 | } 204 | 205 | done(); 206 | }) 207 | 208 | if (!isTestEnv) { 209 | const serverInfo = await fastify.listen({ 210 | port: process.env.PORT || 9999, 211 | host: '::', 212 | }) 213 | 214 | console.log(`server is running at ${serverInfo}`) 215 | } 216 | 217 | export const server = fastify 218 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, before, after, it } from 'node:test' 2 | import { deepStrictEqual, ok } from 'node:assert' 3 | import { runSeed } from '../config/seed.js' 4 | import { users } from '../config/users.js' 5 | 6 | describe('API Workflow', () => { 7 | let _testServer 8 | let _testServerAddress 9 | 10 | function createCustomer(customer) { 11 | return _testServer.inject({ 12 | method: 'POST', 13 | url: `${_testServerAddress}/v1/customers`, 14 | payload: customer, 15 | }) 16 | } 17 | 18 | function getCustomers() { 19 | return _testServer.inject({ 20 | method: 'GET', 21 | url: `${_testServerAddress}/v1/customers`, 22 | }) 23 | } 24 | 25 | function getCustomerById(id) { 26 | return _testServer.inject({ 27 | method: 'GET', 28 | url: `${_testServerAddress}/v1/customers/${id}`, 29 | }) 30 | } 31 | 32 | function updateCustomer(id, customer) { 33 | return _testServer.inject({ 34 | method: 'PUT', 35 | url: `${_testServerAddress}/v1/customers/${id}`, 36 | payload: customer, 37 | }) 38 | } 39 | 40 | function deleteCustomer(id) { 41 | return _testServer.inject({ 42 | method: 'DELETE', 43 | url: `${_testServerAddress}/v1/customers/${id}`, 44 | }) 45 | } 46 | 47 | async function validateUsersListOrderedByName(usersSent) { 48 | const res = await getCustomers() 49 | const statusCode = res.statusCode 50 | const result = await res.json() 51 | 52 | const sort = items => items 53 | .map(({ _id, ...remaining }) => remaining) 54 | .sort((a, b) => a.name.localeCompare(b.name)) 55 | 56 | const expectSortedByName = sort(usersSent) 57 | 58 | deepStrictEqual(statusCode, 200) 59 | deepStrictEqual(sort(result), expectSortedByName) 60 | } 61 | 62 | before(async () => { 63 | const { server } = await import('../src/index.js') 64 | _testServer = server 65 | 66 | _testServerAddress = await server.listen(); 67 | }) 68 | 69 | beforeEach(async () => { 70 | return runSeed() 71 | }) 72 | 73 | after(async () => _testServer.close()) 74 | 75 | describe('POST /v1/customers', () => { 76 | it('should create customer', async () => { 77 | const input = { 78 | name: 'Xuxa da Silva', 79 | phone: '123456789', 80 | } 81 | 82 | const expected = { message: `user ${input.name} created!` } 83 | 84 | 85 | const res = await createCustomer(input) 86 | deepStrictEqual(res.statusCode, 201) 87 | const result = await res.json() 88 | ok(result.id) 89 | delete result.id 90 | deepStrictEqual(result, expected) 91 | 92 | await validateUsersListOrderedByName([...users, input]) 93 | }) 94 | }) 95 | 96 | describe(`GET /v1/customers`, () => { 97 | it('should retrieve only initial users', async () => { 98 | return validateUsersListOrderedByName(users) 99 | }) 100 | 101 | it('given 5 different customers it should have valid list', async () => { 102 | const customers = [ 103 | { name: 'Erick Wendel', phone: '123456789' }, 104 | { name: 'Ana Neri', phone: '123456789' }, 105 | { name: 'Shrek de Souza', phone: '123456789' }, 106 | { name: 'Nemo de Oliveira', phone: '123456789' }, 107 | { name: 'Buzz da Rocha', phone: '123456789' }, 108 | ] 109 | await Promise.all( 110 | customers.map(customer => createCustomer(customer)) 111 | ) 112 | 113 | await validateUsersListOrderedByName(users.concat(customers)) 114 | }) 115 | }) 116 | 117 | describe(`GET /v1/customers/:id`, () => { 118 | it('should retrieve a customer by ID', async () => { 119 | const customerResponse = await createCustomer({ 120 | name: 'Test User', 121 | phone: '123456789', 122 | }) 123 | 124 | const { id } = await customerResponse.json() // Extracting the ID from the response 125 | 126 | const res = await getCustomerById(id) 127 | deepStrictEqual(res.statusCode, 200) 128 | deepStrictEqual(await res.json(), { id, name: 'Test User', phone: '123456789' }) 129 | }) 130 | 131 | it('should return 404 for non-existent customer', async () => { 132 | const res = await getCustomerById('66fbfd09785d518f5c747366') 133 | deepStrictEqual(res.statusCode, 404) 134 | }) 135 | }) 136 | 137 | describe(`PUT /v1/customers/:id`, () => { 138 | it('should update a customer', async () => { 139 | const customerResponse = await createCustomer({ 140 | name: 'Update User', 141 | phone: '123456789', 142 | }) 143 | 144 | const { id } = await customerResponse.json() // Extracting the ID from the response 145 | 146 | const updatedData = { name: 'Updated Name', phone: '987654321' } 147 | const res = await updateCustomer(id, updatedData) 148 | deepStrictEqual(res.statusCode, 200) 149 | deepStrictEqual(await res.json(), { message: `User ${id} updated!`, id }) 150 | 151 | const updatedUser = await getCustomerById(id) 152 | deepStrictEqual(updatedUser.statusCode, 200) 153 | deepStrictEqual(await updatedUser.json(), { id, ...updatedData }) 154 | }) 155 | 156 | it('should return 400 for invalid id', async () => { 157 | const id = '123' 158 | const res = await updateCustomer(id, { name: 'New Name', phone: '123' }) 159 | deepStrictEqual(res.statusCode, 400) 160 | const result = await res.json() 161 | deepStrictEqual(result, { message: 'the id is invalid!', id }) 162 | }) 163 | }) 164 | 165 | describe(`DELETE /v1/customers/:id`, () => { 166 | it('should delete a customer', async () => { 167 | const customerResponse = await createCustomer({ 168 | name: 'Delete User', 169 | phone: '123456789', 170 | }) 171 | 172 | const { id } = await customerResponse.json() // Extracting the ID from the response 173 | 174 | const res = await deleteCustomer(id) 175 | deepStrictEqual(res.statusCode, 200) 176 | deepStrictEqual(await res.json(), { message: `User ${id} deleted!`, id }) 177 | 178 | const deletedUser = await getCustomerById(id) 179 | deepStrictEqual(deletedUser.statusCode, 404) 180 | }) 181 | 182 | it('should return 400 for id invalid', async () => { 183 | const id = '123' 184 | const res = await deleteCustomer(id) 185 | deepStrictEqual(res.statusCode, 400) 186 | deepStrictEqual(await res.json(), { message: 'the id is invalid!', id }) 187 | }) 188 | }) 189 | }) 190 | --------------------------------------------------------------------------------