diff --git a/.cliamrc.js b/.cliamrc.js index e3b1a75..1e6c3cc 100644 --- a/.cliamrc.js +++ b/.cliamrc.js @@ -17,7 +17,6 @@ module.exports = { transporters: [ { id: 'smtp-transporter', - mode: 'smtp', auth: { username: "noemie2@ethereal.email", password: "5cDumYP68aFcpT15uV" diff --git a/src/env/template.env b/.env.example similarity index 97% rename from src/env/template.env rename to .env.example index bc0412d..f59fab8 100644 --- a/src/env/template.env +++ b/.env.example @@ -98,12 +98,12 @@ REFRESH_TOKEN_SECRET = "hello-i-am-a-refresh-token-secret-passphrase" # SSL_KEY = "path-to-my-ssl-key.pem" # Database engine -TYPEORM_TYPE = "mysql" +TYPEORM_TYPE = "mariadb" # Database connection identifier # TYPEORM_NAME = "default" -# Database server host +# Database server host. Use "db" with docker-compose, localhost otherwise. TYPEORM_HOST = "localhost" # Database name. Keep it different from your developement database. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca32781..1ac342f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,18 +16,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20.9.0' - - name: Create directories - run: mkdir -p ./dist/env && cp ./src/env/template.env ./dist/env/test.env - - name: Setup .env file - run: | - echo FACEBOOK_CONSUMER_ID = "${{ secrets.FACEBOOK_CONSUMER_ID }}" >> ./dist/env/test.env - echo FACEBOOK_CONSUMER_SECRET = "${{ secrets.FACEBOOK_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo GITHUB_CONSUMER_ID = "${{ secrets.GTHB_CONSUMER_ID }}" >> ./dist/env/test.env - echo GITHUB_CONSUMER_SECRET = "${{ secrets.GTHB_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo GOOGLE_CONSUMER_ID = "${{ secrets.GOOGLE_CONSUMER_ID }}" >> ./dist/env/test.env - echo GOOGLE_CONSUMER_SECRET = "${{ secrets.GOOGLE_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo LINKEDIN_CONSUMER_ID = "${{ secrets.LINKEDIN_CONSUMER_ID }}" >> ./dist/env/test.env - echo LINKEDIN_CONSUMER_SECRET = "${{ secrets.LINKEDIN_CONSUMER_SECRET }}" >> ./dist/env/test.env - name: Install global dependencies run: npm i typescript@5.7.2 -g - name: Install local dependencies @@ -60,11 +48,23 @@ jobs: with: mysql version: '5.7' mysql database: 'typeplate_test' - mysql root password: passw0rd + mysql root password: passw0rd - name: Install global dependencies run: npm i typescript@5.7.2 -g - name: Install local dependencies run: npm i + - name: Copy .env.example to .env.test + run: cp .env.example .env.test + - name: Setup .env file + run: | + echo FACEBOOK_CONSUMER_ID = "${{ secrets.FACEBOOK_CONSUMER_ID }}" >> .env.test + echo FACEBOOK_CONSUMER_SECRET = "${{ secrets.FACEBOOK_CONSUMER_SECRET }}" >> .env.test + echo GITHUB_CONSUMER_ID = "${{ secrets.GTHB_CONSUMER_ID }}" >> .env.test + echo GITHUB_CONSUMER_SECRET = "${{ secrets.GTHB_CONSUMER_SECRET }}" >> .env.test + echo GOOGLE_CONSUMER_ID = "${{ secrets.GOOGLE_CONSUMER_ID }}" >> .env.test + echo GOOGLE_CONSUMER_SECRET = "${{ secrets.GOOGLE_CONSUMER_SECRET }}" >> .env.test + echo LINKEDIN_CONSUMER_ID = "${{ secrets.LINKEDIN_CONSUMER_ID }}" >> .env.test + echo LINKEDIN_CONSUMER_SECRET = "${{ secrets.LINKEDIN_CONSUMER_SECRET }}" >> .env.test - name: Create dist directory run: mkdir dist - name: Download build artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbfc969..21df99c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,18 +17,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20.9.0' - - name: Create directories - run: mkdir -p ./dist/env && cp ./src/env/template.env ./dist/env/test.env - - name: Setup .env file - run: | - echo FACEBOOK_CONSUMER_ID = "${{ secrets.FACEBOOK_CONSUMER_ID }}" >> ./dist/env/test.env - echo FACEBOOK_CONSUMER_SECRET = "${{ secrets.FACEBOOK_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo GITHUB_CONSUMER_ID = "${{ secrets.GTHB_CONSUMER_ID }}" >> ./dist/env/test.env - echo GITHUB_CONSUMER_SECRET = "${{ secrets.GTHB_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo GOOGLE_CONSUMER_ID = "${{ secrets.GOOGLE_CONSUMER_ID }}" >> ./dist/env/test.env - echo GOOGLE_CONSUMER_SECRET = "${{ secrets.GOOGLE_CONSUMER_SECRET }}" >> ./dist/env/test.env - echo LINKEDIN_CONSUMER_ID = "${{ secrets.LINKEDIN_CONSUMER_ID }}" >> ./dist/env/test.env - echo LINKEDIN_CONSUMER_SECRET = "${{ secrets.LINKEDIN_CONSUMER_SECRET }}" >> ./dist/env/test.env - name: Install global dependencies run: npm i typescript@5.7.2 -g - name: Install local dependencies @@ -61,11 +49,23 @@ jobs: with: mysql version: '5.7' mysql database: 'typeplate_test' - mysql root password: passw0rd + mysql root password: passw0rd - name: Install global dependencies run: npm i typescript@5.7.2 -g - name: Install local dependencies run: npm i + - name: Copy .env.example to .env.test + run: cp .env.example .env.test + - name: Setup .env file + run: | + echo FACEBOOK_CONSUMER_ID = "${{ secrets.FACEBOOK_CONSUMER_ID }}" >> .env.test + echo FACEBOOK_CONSUMER_SECRET = "${{ secrets.FACEBOOK_CONSUMER_SECRET }}" >> .env.test + echo GITHUB_CONSUMER_ID = "${{ secrets.GTHB_CONSUMER_ID }}" >> .env.test + echo GITHUB_CONSUMER_SECRET = "${{ secrets.GTHB_CONSUMER_SECRET }}" >> .env.test + echo GOOGLE_CONSUMER_ID = "${{ secrets.GOOGLE_CONSUMER_ID }}" >> .env.test + echo GOOGLE_CONSUMER_SECRET = "${{ secrets.GOOGLE_CONSUMER_SECRET }}" >> .env.test + echo LINKEDIN_CONSUMER_ID = "${{ secrets.LINKEDIN_CONSUMER_ID }}" >> .env.test + echo LINKEDIN_CONSUMER_SECRET = "${{ secrets.LINKEDIN_CONSUMER_SECRET }}" >> .env.test - name: Create dist directory run: mkdir dist - name: Download build artifact @@ -77,7 +77,7 @@ jobs: run: npm run schema:sync - name: Execute tests suites run: npm run ci:test - - name: Publish to coveralls.io + - name: Publish coverage to coveralls.io uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index efe9b19..8f948cc 100644 --- a/.gitignore +++ b/.gitignore @@ -76,10 +76,10 @@ docs/ reports/ # .env files -/**/env/development.env -/**/env/production.env -/**/env/staging.env -/**/env/test.env +.env.development +.env.test +.env.staging +.env.production # Husky configuration .husky/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f832e03 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Build stage +FROM node:20.18.0-alpine AS builder + +WORKDIR /app + +# Install build dependencies +COPY package*.json ./ +RUN npm ci + +# Copy source code +COPY . . + +# Build TypeScript code and create env files +RUN npm run init:copy && npm run init:compile + +# Production stage +FROM node:20.18.0-alpine AS production + +WORKDIR /app + +# Install production dependencies only +COPY package*.json ./ +RUN npm i +RUN npm i -g nodemon + +# Copy built files and config files +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/.cliamrc.js ./.cliamrc.js + +# Create necessary directories for uploads and logs +RUN mkdir -p ./dist/public/archives \ + && mkdir -p ./dist/public/documents \ + && mkdir -p ./dist/public/images/master-copy \ + && mkdir -p ./dist/public/images/rescale \ + && mkdir -p ./dist/public/audios \ + && mkdir -p ./dist/public/videos \ + && mkdir -p ./dist/logs + +# Set environment variables +ENV NODE_ENV=development +ENV PORT=8101 + +# Expose the port +EXPOSE 8101 + +# Start the application +CMD ["nodemon", "."] diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 0000000..cc58fce --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,46 @@ +# Build stage +FROM node:20.18.0-alpine AS builder + +WORKDIR /app + +# Install build dependencies +COPY package*.json ./ +RUN npm ci + +# Copy source code +COPY . . + +# Build TypeScript code and create env files +RUN npm run init:copy && npm run init:compile + +# Production stage +FROM node:20.18.0-alpine AS production + +WORKDIR /app + +# Install production dependencies only +COPY package*.json ./ +RUN npm ci --only=production + +# Copy built files and config files +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/.cliamrc.js ./.cliamrc.js + +# Create necessary directories for uploads and logs +RUN mkdir -p ./dist/public/archives \ + && mkdir -p ./dist/public/documents \ + && mkdir -p ./dist/public/images/master-copy \ + && mkdir -p ./dist/public/images/rescale \ + && mkdir -p ./dist/public/audios \ + && mkdir -p ./dist/public/videos \ + && mkdir -p ./dist/logs + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=8101 + +# Expose the port +EXPOSE 8101 + +# Start the application +CMD ["node", "./dist/api/app.bootstrap.js"] diff --git a/README.md b/README.md index 9e00a83..ae27261 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Typescript / Express / Typeorm REST API boilerplate](https://i.ibb.co/dM2bhNJ/cover-typeplate.png) -[![Node](https://img.shields.io/badge/Node-18.19.0-informational?logo=node.js&color=43853D)](https://nodejs.org/docs/latest-v18.x/api/index.html) +[![Node](https://img.shields.io/badge/Node-20.18.0-informational?logo=node.js&color=43853D)](https://nodejs.org/docs/latest-v20.x/api/index.html) [![TypeScript](https://img.shields.io/badge/Typescript-5.7.2-informational?logo=typescript&color=2F74C0)](https://www.typescriptlang.org/) [![Express](https://img.shields.io/badge/Express-4.21.2-informational?logo=express&color=B1B1B1)](https://expressjs.com/) [![Typeorm](https://img.shields.io/badge/Typeorm-0.3.20-informational?logo=typeorm&color=FFAB00)](https://typeorm.io/#/) @@ -18,6 +18,9 @@ Thanks to Daniel F. Sousa for inspiration with his [ExpressREST API boilerplate] ## > Features +- **Containerization** + - **Docker** with [Dockerfile](https://docs.docker.com/get-started/). + - **Docker compose** with [docker-compose.yml](https://docs.docker.com/compose/) with mariadb and phpmyadmin. - **Basics** - **Clear & clean code architecture** with classic layers such controllers, services, repositories, models, ... - **Object Relational Mapping** with [typeorm](https://typeorm.io/#/). @@ -48,48 +51,48 @@ Thanks to Daniel F. Sousa for inspiration with his [ExpressREST API boilerplate] ## > Table of contents -* [Getting started](#getting-started) -* [Entity generation](#entity-generation) -* [Documentation](#documentation) -* [Tests](#tests) -* [Continuous integration](#continuous-integration) -* [Deployment](#deployment) -* [Licence](#licence) +- [Getting started](#getting-started) +- [Entity generation](#entity-generation) +- [Documentation](#documentation) +- [Tests](#tests) +- [Continuous integration](#continuous-integration) +- [Deployment](#deployment) +- [License](#license) ## Getting started ### Prerequisites -* Git -* Node.js >= 18.19.0 -* NPM >= 10.2.3 -* A database engine with a dedicated database +- Git +- Node.js >= 20.18.0 +- NPM >= 10.2.3 +- A database engine with a dedicated database (MariaDB, MySQL, PostgreSQL, ...) or Docker ### Step 1: install -```bash -$ git clone https://github.com/steve-lebleu/typeplate.git path-to/your-project-name/ -``` - -### Step 2: go to +#### Local ```bash -$ cd path-to/your-project-name/ +git clone https://github.com/steve-lebleu/typeplate.git path-to/your-project-name/ +cd path-to/your-project-name/ +npm run init ``` -### Step 3: build +#### Docker ```bash -$ npm run init +docker compose build ``` -### Step 4: setup package.json +### Step 2: setup package.json Open the *./package.json* file and edit it with your own values. -### Step 5: setup environment +### Step 3: setup environment -Open *./dist/env/development.env* and fill the required env variables (uncommented in the file). See wiki [env variables list](https://github.com/steve-lebleu/typeplate/wiki/Environment-variables) for more informations. +Open *./dist/env/development.env* and fill the required env variables (uncommented in the file). + +See wiki [env variables list](https://github.com/steve-lebleu/typeplate/wiki/Environment-variables) for more informations. ```bash # Access token Secret passphrase @@ -110,7 +113,7 @@ REFRESH_TOKEN_SECRET = "your-secret" # Database engine TYPEORM_TYPE = "mysql" -# Database server host +# Database server host. Use "db" with docker-compose, "localhost" otherwise. TYPEORM_HOST = "localhost" # Database name. Keep it different from your developement database. @@ -126,16 +129,24 @@ TYPEORM_PWD = "" TYPEORM_PORT = "3306" ``` -### Step 6: setup cliamrc.js +### Step 4: setup cliamrc.js Transactional emails are send with [cliam](https://github.com/steve-lebleu/cliam). Open the *.cliamrc.js* and fill the [required configuration](https://github.com/steve-lebleu/cliam/wiki/Configuration-with-cliamrc.js) according your sending mode. See Cliam official [documentation](https://github.com/steve-lebleu/cliam/wiki) for more information. Sandbox is set to true by default. -### Step 7: run +### Step 5: run + +#### Local run ```bash -$ nodemon +nodemon +``` + +#### Docker run + +```bash +docker compose up -d ``` ## Entity generation @@ -147,7 +158,7 @@ See [entity generation](https://github.com/steve-lebleu/typeplate/wiki/Entity-ge ## Documentation ```bash -$ npm run doc +npm run doc ``` Generate API documentation website into *./docs/apidoc/*. @@ -157,7 +168,7 @@ See [apidoc](http://apidocjs.com/) for more informations about customization. ## Tests ```bash -$ npm run test +npm run test ``` :warning: Because there are integration tests, you need a dedicated database to run the tests suites. It's already managed in CI/CD using Github actions, You need to mount it yourself in your local environment. @@ -177,7 +188,7 @@ Project implements a basic [PM2](https://github.com/Unitech/PM2/) configuration Install PM2 globaly : ```bash -$ npm i pm2 -g +npm i pm2 -g ``` ### Configuration @@ -200,6 +211,7 @@ Configure the *./ecosystem.config.js* file with your env informations. } } ``` + More info about PM2 [ecosystem.config.js](https://pm2.io/doc/en/runtime/reference/ecosystem-file/) file. ### Deploy @@ -208,10 +220,10 @@ Pm 2 must be installed on the target server and your SSH public key granted. ```bash # Setup deployment at remote location -$ pm2 deploy production setup +pm2 deploy production setup # Update remote version -$ pm2 deploy production update +pm2 deploy production update ``` More info about [PM2](http://pm2.keymetrics.io/docs/usage/quick-start/) and [PM2 deploy](https://pm2.io/doc/en/runtime/guide/easy-deploy-with-ssh/). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..875c432 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +services: + api: + build: + context: . + dockerfile: Dockerfile + args: + - NODE_ENV=development + ports: + - ${PORT}:${PORT} + depends_on: + db: + condition: service_healthy + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + develop: + watch: + - action: sync + path: ./ + target: /app + + db: + image: mariadb + restart: always + user: ${TYPEORM_USER} + volumes: + - db-data:/var/lib/mysql + env_file: + - development.env + environment: + - MARIADB_ROOT_PASSWORD=${TYPEORM_PWD} + - MARIADB_DATABASE=${TYPEORM_DB} + expose: + - 3306 + healthcheck: + test: + [ + "CMD", + "/usr/local/bin/healthcheck.sh", + "--su-mysql", + "--connect", + "--innodb_initialized", + ] + interval: 5s + timeout: 5s + retries: 5 + + phpmyadmin: + image: phpmyadmin + ports: + - 8080:80 + depends_on: + - db + environment: + - PMA_HOST=db + +volumes: + db-data: \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 99734ee..a48567f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "typeplate", - "version": "1.6.4", + "version": "1.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "typeplate", - "version": "1.6.4", + "version": "1.6.5", "license": "MIT", "dependencies": { "@hapi/boom": "9.1.4", @@ -19,7 +19,7 @@ "es6-promisify": "7.0.0", "express": "^4.21.2", "express-rate-limit": "^7.5.0", - "filenamify": "^6.0.0", + "filenamify": "^4.3.0", "helmet": "^8.0.0", "hpp": "0.2.3", "http-status": "^1.8.1", @@ -65,7 +65,6 @@ "eslint-plugin-jsdoc": "32.2.0", "eslint-plugin-prefer-arrow": "1.2.3", "mocha": "^10.3.0", - "node-notifier": "^10.0.1", "nodemon": "^3.1.9", "npm-run-all": "4.1.5", "nyc": "15.1.0", @@ -792,342 +791,6 @@ "node": ">=10.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", - "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", - "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", - "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", - "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", - "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", - "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", - "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", - "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", - "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", - "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", - "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", - "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", - "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", - "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", - "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", - "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", - "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", - "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", - "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", - "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", - "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", @@ -5964,27 +5627,26 @@ } }, "node_modules/filename-reserved-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/filenamify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", - "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", "license": "MIT", "dependencies": { - "filename-reserved-regex": "^3.0.0" + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" }, "engines": { - "node": ">=16" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6279,20 +5941,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6597,12 +6245,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", - "dev": true - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -7294,21 +6936,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7558,18 +7185,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9334,30 +8949,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/node-notifier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", - "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==", - "dev": true, - "dependencies": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.5", - "shellwords": "^0.1.1", - "uuid": "^8.3.2", - "which": "^2.0.2" - } - }, - "node_modules/node-notifier/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -11801,12 +11392,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -12283,6 +11868,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", @@ -12730,6 +12336,27 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", diff --git a/package.json b/package.json index ddf63ed..9d3f958 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ }, "scripts": { "init": "npm-run-all -s init:*", - "init:install": "npm i typescript@5.3.3 -g && npm i typeorm@0.3.20 -g && npm i rsgen -g && npm i ", - "init:copy": "mkdir -p ./dist/env && cp ./src/env/template.env ./dist/env/development.env && cp ./src/env/template.env ./dist/env/test.env && cp ./src/env/template.env ./dist/env/staging.env && cp ./src/env/template.env ./dist/env/production.env", + "init:install": "npm i typescript@5.7.2 -g && npm i typeorm@0.3.20 -g && npm i rsgen -g && npm i ", + "init:copy": "cp .env.example .env.development && cp .env.example .env.test && cp .env.example .env.production", "init:compile": "tsc", "init:version": "rm -rf ./.git && git init && git add . --all && git commit -m \"First commit\"", "ci:test": "nyc --reporter=lcov --report-dir=./reports/coverage npm-run-all -s test:*", @@ -86,9 +86,9 @@ "dayjs": "^1.11.13", "dotenv": "^16.4.7", "es6-promisify": "7.0.0", - "express": "^4.21.2", + "express": "4.21.2", "express-rate-limit": "^7.5.0", - "filenamify": "^6.0.0", + "filenamify": "^4.3.0", "helmet": "^8.0.0", "hpp": "0.2.3", "http-status": "^1.8.1", @@ -134,7 +134,6 @@ "eslint-plugin-jsdoc": "32.2.0", "eslint-plugin-prefer-arrow": "1.2.3", "mocha": "^10.3.0", - "node-notifier": "^10.0.1", "nodemon": "^3.1.9", "npm-run-all": "4.1.5", "nyc": "15.1.0", diff --git a/src/api/app.bootstrap.ts b/src/api/app.bootstrap.ts index 35c59ee..beb6761 100644 --- a/src/api/app.bootstrap.ts +++ b/src/api/app.bootstrap.ts @@ -10,7 +10,7 @@ ApplicationDataSource.initialize() Logger.log('info', `Connection to MySQL server established on port ${TYPEORM.PORT} (${ENV})`); }) .catch((error: Error) => { - process.stdout.write(`error: ${error.message}`); + console.error('Error while connecting to database:', error.message); process.exit(1); }); diff --git a/src/api/config/app.config.ts b/src/api/config/app.config.ts index 689a822..6335ff1 100644 --- a/src/api/config/app.config.ts +++ b/src/api/config/app.config.ts @@ -188,13 +188,6 @@ export class ExpressConfiguration { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.application.use(`/api/${API_VERSION}`, RateLimit(this.options.rate), Cache.read, ProxyRouter.map(), Sanitize.sanitize, Resolve.write); - /** - * Desktop error notification - */ - if( [ENVIRONMENT.development].includes(ENV as ENVIRONMENT) ) { - this.application.use( Catch.notification ); - } - /** * Lifecycle of an error request * diff --git a/src/api/config/environment.config.ts b/src/api/config/environment.config.ts index 8bfdab2..0c43b3d 100644 --- a/src/api/config/environment.config.ts +++ b/src/api/config/environment.config.ts @@ -722,7 +722,7 @@ export class Environment { this.environment = ENVIRONMENT[process.env.NODE_ENV as ENVIRONMENT]; } - const path = `${process.cwd()}/${this.base}/env/${this.environment}.env`; + const path = `${process.cwd()}/.env.${this.environment}`; if (!existsSync(path)) { this.exit(`Environment file not found at ${path}`); diff --git a/src/api/config/logger.config.ts b/src/api/config/logger.config.ts index 0e1035a..b4fe8a2 100644 --- a/src/api/config/logger.config.ts +++ b/src/api/config/logger.config.ts @@ -37,7 +37,7 @@ class LoggerConfiguration { * @description Output format */ private formater = format.printf( ( { level, message, label, timestamp } ) => { - return `${timestamp as string} [${level}] ${message}`; + return `${timestamp as string} [${level}] ${message as string}`; }); /** diff --git a/src/api/config/upload.config.ts b/src/api/config/upload.config.ts index fd549ce..d415620 100644 --- a/src/api/config/upload.config.ts +++ b/src/api/config/upload.config.ts @@ -1,6 +1,5 @@ import * as Multer from 'multer'; - -import filenamify from 'filenamify'; +import * as filenamify from 'filenamify'; import { existsSync, mkdirSync } from 'fs'; import { unsupportedMediaType } from '@hapi/boom'; diff --git a/src/api/core/middlewares/catch.middleware.ts b/src/api/core/middlewares/catch.middleware.ts index d6239ba..d441a8f 100644 --- a/src/api/core/middlewares/catch.middleware.ts +++ b/src/api/core/middlewares/catch.middleware.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import { notify } from 'node-notifier'; import { Logger } from '@services/logger.service'; import { ErrorFactory } from '@factories/error.factory'; @@ -32,25 +31,6 @@ class Catch { return Catch.instance; } - /** - * @description Display error in desktop notification - * - * @param err Error object - * @param req Express request object derived from http.incomingMessage - * @param res Express response object - * @param next Callback function - * - * @require libnotify-bin - * @require node-notifier - */ - notification(err: Error, req: Request, res: Response, next: (e: Error, req, res, next) => void): void { - notify({ - title: `Error in ${req.method} ${req.url}`, - message : err.name + '\n' + err.stack ? err.stack : err.message - }); - next(err, req, res, next); - } - /** * @description * diff --git a/test/units/01-express-app.unit.test.js b/test/units/01-express-app.unit.test.js index c0c51ea..59822f9 100644 --- a/test/units/01-express-app.unit.test.js +++ b/test/units/01-express-app.unit.test.js @@ -9,8 +9,8 @@ describe('Express application', () => { expect(typeof(application)).to.equal('function'); }); - it('Express server version is 4.18.2', () => { - expect(pkgInfo.dependencies.express).to.equal('4.18.2'); + it('Express server version is 4.21.2', () => { + expect(pkgInfo.dependencies.express).to.equal('4.21.2'); }); }); \ No newline at end of file diff --git a/test/units/05-middlewares.unit.test.js b/test/units/05-middlewares.unit.test.js index 529eeab..53b533c 100644 --- a/test/units/05-middlewares.unit.test.js +++ b/test/units/05-middlewares.unit.test.js @@ -76,15 +76,11 @@ describe('Middlewares', () => { describe('Uploader', () => { it('should mix args options', (done) => { - - Uploader.upload( { maxFiles: 2, filesize: 5000 } )( null, null, (e) => e ) - .then(r => { - expect(Uploader.options.maxFiles).to.be.eqls(2); - expect(Uploader.options.filesize).to.be.eqls(5000); - done(); - }) - .catch(e => { done(e); }); - + Uploader.upload( { maxFiles: 2, filesize: 5000 } )( null, null, (e) => { + expect(Uploader.options.maxFiles).to.be.eqls(2); + expect(Uploader.options.filesize).to.be.eqls(5000); + done(); + }) }); });