From 4bfc714cd2f51aff81197dd00204ff4fb13514a9 Mon Sep 17 00:00:00 2001 From: Steve Lebleu Date: Fri, 16 Feb 2024 10:48:20 +0100 Subject: [PATCH] Feature: update typeorm (#67) * fix: type string to type ENVIRONMENT * chore: upgrade TypeORM to 0.3.20 aka @latest * fix: unit tests, some typeorm syntaxes * fix: datasource definition * fix: typeorm cli not able to retrieve commonJS module * fix: datasource for CLI * fix: adapt typeorm tasks * chore: remove ormconfig.json * chore: embed templates in project * feat: embed templates for generation, update readme --- .github/workflows/build.yml | 6 +- .github/workflows/release.yml | 6 +- README.md | 3 +- ormconfig.json | 25 -- package-lock.json | 230 +++++++++++++++--- package.json | 12 +- src/.eslintrc.js | 3 +- src/api/app.bootstrap.ts | 17 +- src/api/config/database.config.ts | 51 ++-- src/api/config/datasource.config.ts | 35 +++ src/api/config/environment.config.ts | 6 +- src/api/core/controllers/auth.controller.ts | 24 +- src/api/core/controllers/media.controller.ts | 23 +- src/api/core/controllers/user.controller.ts | 25 +- src/api/core/middlewares/guard.middleware.ts | 2 +- src/api/core/models/media.model.ts | 2 +- src/api/core/models/user.model.ts | 33 ++- src/api/core/repositories/media.repository.ts | 20 +- .../repositories/refresh-token.repository.ts | 16 +- src/api/core/repositories/user.repository.ts | 37 ++- src/api/core/services/auth.service.ts | 23 +- .../types/interfaces/request.interface.ts | 2 +- src/templates/business.service.txt | 56 +++++ src/templates/controller.txt | 116 +++++++++ src/templates/data-layer.service.txt | 104 ++++++++ src/templates/fixture.txt | 5 + src/templates/model.txt | 45 ++++ src/templates/query-string.interface.txt | 3 + src/templates/repository.txt | 53 ++++ src/templates/request.interface.txt | 8 + src/templates/route.txt | 116 +++++++++ src/templates/subscriber.txt | 50 ++++ src/templates/test.txt | 130 ++++++++++ src/templates/validation.txt | 52 ++++ test/units/00-application.unit.test.js | 2 +- test/units/01-express-app.unit.test.js | 2 +- test/units/02-config.unit.test.js | 7 +- 37 files changed, 1116 insertions(+), 234 deletions(-) delete mode 100644 ormconfig.json create mode 100644 src/api/config/datasource.config.ts create mode 100644 src/templates/business.service.txt create mode 100644 src/templates/controller.txt create mode 100644 src/templates/data-layer.service.txt create mode 100644 src/templates/fixture.txt create mode 100644 src/templates/model.txt create mode 100644 src/templates/query-string.interface.txt create mode 100644 src/templates/repository.txt create mode 100644 src/templates/request.interface.txt create mode 100644 src/templates/route.txt create mode 100644 src/templates/subscriber.txt create mode 100644 src/templates/test.txt create mode 100644 src/templates/validation.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c9a503..273ef24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: 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.3.3 -g && npm i typeorm@0.2.41 -g + run: npm i typescript@5.3.3 -g - name: Install local dependencies run: npm i - name: Compile Typescript files @@ -62,7 +62,7 @@ jobs: mysql database: 'typeplate_test' mysql root password: passw0rd - name: Install global dependencies - run: npm i typescript@5.3.3 -g && npm i typeorm@0.2.41 -g + run: npm i typescript@5.3.3 -g - name: Install local dependencies run: npm i - name: Create dist directory @@ -73,7 +73,7 @@ jobs: name: build-files path: dist - name: Synchronize database schema - run: typeorm schema:sync + run: npm run schema:sync - name: Execute tests suites run: npm run ci:test - name: Publish to coveralls.io diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index def766c..8999ae1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: 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.3.3 -g && npm i typeorm@0.2.41 -g + run: npm i typescript@5.3.3 -g - name: Install local dependencies run: npm i - name: Compile Typescript files @@ -63,7 +63,7 @@ jobs: mysql database: 'typeplate_test' mysql root password: passw0rd - name: Install global dependencies - run: npm i typescript@5.3.3 -g && npm i typeorm@0.2.41 -g + run: npm i typescript@5.3.3 -g - name: Install local dependencies run: npm i - name: Create dist directory @@ -74,7 +74,7 @@ jobs: name: build-files path: dist - name: Synchronize database schema - run: typeorm schema:sync + run: npm run schema:sync - name: Execute tests suites run: npm run ci:test - name: Publish to coveralls.io diff --git a/README.md b/README.md index 443e306..aba926b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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) [![TypeScript](https://img.shields.io/badge/Typescript-5.3.3-informational?logo=typescript&color=2F74C0)](https://www.typescriptlang.org/) [![Express](https://img.shields.io/badge/Express-4.18.2-informational?logo=express&color=B1B1B1)](https://expressjs.com/) -[![Typeorm](https://img.shields.io/badge/Typeorm-0.2.41-informational?logo=typeorm&color=FFAB00)](https://typeorm.io/#/) +[![Typeorm](https://img.shields.io/badge/Typeorm-0.3.20-informational?logo=typeorm&color=FFAB00)](https://typeorm.io/#/) [![Mocha](https://img.shields.io/badge/Mocha-10.3.0-informational?logo=mocha&color=8A6343)](https://mochajs.org) ![Github action workflow status](https://github.com/steve-lebleu/typeplate/actions/workflows/build.yml/badge.svg?branch=master) @@ -16,6 +16,7 @@ Ready to use RESTful API boilerplate builded with [Express.js](http://expressjs.com/en/4x/api.html), [Typescript](https://github.com/Microsoft/TypeScript) [TypeORM](https://github.com/typeorm/typeorm) and [Mocha](https://mochajs.org/). 🤘 Thanks to Daniel F. Sousa for inspiration with her [Express ES2017 REST API boilerplate](https://github.com/danielfsousa/express-rest-boilerplate) :beer: :beer: :beer: + ## > Features - **Basics** diff --git a/ormconfig.json b/ormconfig.json deleted file mode 100644 index 087f1e7..0000000 --- a/ormconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "type": "mysql", - "name": "default", - "host": "localhost", - "port": 3306, - "username": "root", - "password": "passw0rd", - "database": "typeplate_test", - "synchronize": true, - "logging": false, - "entities": [ - "dist/api/core/models/**/*.model.js", - "dist/api/resources/**/*.model.js" - ], - "migrations": [ - "dist/migrations/**/*.js" - ], - "subscribers": [ - "dist/api/core/subscribers/**/*.subscriber.js", - "dist/api/resources/**/*.subscriber.js" - ], - "cli": { - "migrationsDir": "./dist/migrations" - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2b60620..117f180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "passport-linkedin-oauth2": "2.0.0", "pluralize": "8.0.0", "reflect-metadata": "^0.2.1", - "typeorm": "^0.2.41", + "typeorm": "^0.3.0", "uuid": "8.3.2", "winston": "^3.11.0" }, @@ -72,6 +72,7 @@ "rsgen": "^0.3.2", "sinon": "^17.0.1", "supertest": "^6.3.4", + "ts-node": "^10.9.2", "typescript": "^5.3.3", "typescript-eslint": "0.0.1-alpha.0" }, @@ -516,6 +517,22 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -750,6 +767,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -2013,6 +2052,30 @@ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", @@ -2225,11 +2288,6 @@ "integrity": "sha512-0LbEEx1zxrYB3pgpd1M5lEhLcXjKJnYghvhTRgaBeUivLHMDM1TzF3IJ6hXU2+8uA4Xz+5BA63mtZo5DjVT8iA==", "dev": true }, - "node_modules/@types/zen-observable": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", - "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.19.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", @@ -2676,6 +2734,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/addressparser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", @@ -2998,6 +3065,12 @@ "node": ">=10" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4291,6 +4364,12 @@ "node": ">=6" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8247,6 +8326,12 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/mandrill-api": { "version": "1.0.45", "resolved": "https://registry.npmjs.org/mandrill-api/-/mandrill-api-1.0.45.tgz", @@ -12149,6 +12234,70 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -12325,31 +12474,34 @@ } }, "node_modules/typeorm": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.45.tgz", - "integrity": "sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.0.tgz", + "integrity": "sha512-fGhJql31DRyxT0bxcjD5kAf9hz+aUppp90M93GmlRlCfHq+qKhY70eCEreAgjrlAYmZkfEgDcyMHpcAfrtCe7A==", "dependencies": { "@sqltools/formatter": "^1.2.2", "app-root-path": "^3.0.0", "buffer": "^6.0.3", "chalk": "^4.1.0", "cli-highlight": "^2.1.11", - "debug": "^4.3.1", - "dotenv": "^8.2.0", - "glob": "^7.1.6", - "js-yaml": "^4.0.0", + "date-fns": "^2.28.0", + "debug": "^4.3.3", + "dotenv": "^16.0.0", + "glob": "^7.2.0", + "js-yaml": "^4.1.0", "mkdirp": "^1.0.4", "reflect-metadata": "^0.1.13", "sha.js": "^2.4.11", - "tslib": "^2.1.0", + "tslib": "^2.3.1", "uuid": "^8.3.2", "xml2js": "^0.4.23", - "yargs": "^17.0.1", - "zen-observable-ts": "^1.0.0" + "yargs": "^17.3.1" }, "bin": { "typeorm": "cli.js" }, + "engines": { + "node": ">= 12.9.0" + }, "funding": { "url": "https://opencollective.com/typeorm" }, @@ -12459,12 +12611,19 @@ "node": ">=12" } }, - "node_modules/typeorm/node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "node_modules/typeorm/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, "engines": { - "node": ">=10" + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" } }, "node_modules/typeorm/node_modules/js-yaml": { @@ -12732,6 +12891,12 @@ "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13292,6 +13457,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -13303,20 +13477,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "node_modules/zen-observable-ts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", - "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", - "dependencies": { - "@types/zen-observable": "0.8.3", - "zen-observable": "0.8.15" - } } } } diff --git a/package.json b/package.json index acd23b1..a39fb19 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,13 @@ "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:*", "doc": "apidoc -i src/ -o docs/apidoc/", - "start": "nodemon", + "start": "nodemon .", "test": "nyc --reporter=html --report-dir=./reports/nyc-coverage npm-run-all -s test:*", "test:unit": "./node_modules/.bin/mocha ./test/units/00-application.unit.test.js --exit --reporter spec --timeout 10000 --env test", - "test:e2e": "typeorm schema:drop && typeorm schema:sync && ./node_modules/.bin/mocha ./test/e2e/00-api.e2e.test.js --exit --reporter spec --timeout 10000 --env test", + "test:e2e": "npm run schema:drop && npm run schema:sync && ./node_modules/.bin/mocha ./test/e2e/00-api.e2e.test.js --exit --reporter spec --timeout 10000 --env test", + "typeorm": "npx typeorm ", + "schema:drop": "npm run typeorm schema:drop -- -d dist/api/config/datasource.config.js", + "schema:sync": "npm run typeorm schema:drop -- -d dist/api/config/datasource.config.js", "version": "git add package.json && git add README.md && auto-changelog -p && git add CHANGELOG.md && git commit -m \"Update changelog\" --no-verify" }, "_moduleAliases": { @@ -106,7 +109,7 @@ "passport-linkedin-oauth2": "2.0.0", "pluralize": "8.0.0", "reflect-metadata": "^0.2.1", - "typeorm": "^0.2.41", + "typeorm": "^0.3.0", "uuid": "8.3.2", "winston": "^3.11.0" }, @@ -134,9 +137,10 @@ "nodemon": "^3.0.3", "npm-run-all": "4.1.5", "nyc": "15.1.0", - "rsgen": "^0.3.2", + "rsgen": "^0.4.1", "sinon": "^17.0.1", "supertest": "^6.3.4", + "ts-node": "^10.9.2", "typescript": "^5.3.3", "typescript-eslint": "0.0.1-alpha.0" } diff --git a/src/.eslintrc.js b/src/.eslintrc.js index df70fb9..3d08e17 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -78,8 +78,9 @@ module.exports = { "hoist": "all" } ], + "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "error", + "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unused-expressions": "error", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-use-before-define": "error", diff --git a/src/api/app.bootstrap.ts b/src/api/app.bootstrap.ts index 75be8fb..35c59ee 100644 --- a/src/api/app.bootstrap.ts +++ b/src/api/app.bootstrap.ts @@ -1,11 +1,20 @@ require('module-alias/register'); -import { TYPEORM } from '@config/environment.config'; -import { Database } from '@config/database.config'; +import { TYPEORM, ENV } from '@config/environment.config'; +import { Logger } from '@services/logger.service'; +import { ApplicationDataSource } from '@config/database.config'; import { Server } from '@config/server.config'; -import { Application } from '@config/app.config'; -Database.connect(TYPEORM); +ApplicationDataSource.initialize() + .then(() => { + Logger.log('info', `Connection to MySQL server established on port ${TYPEORM.PORT} (${ENV})`); + }) + .catch((error: Error) => { + process.stdout.write(`error: ${error.message}`); + process.exit(1); + }); + +import { Application } from '@config/app.config'; const application = Application; const server = Server.init(application).listen() as unknown; diff --git a/src/api/config/database.config.ts b/src/api/config/database.config.ts index df6f41d..2ee63f7 100644 --- a/src/api/config/database.config.ts +++ b/src/api/config/database.config.ts @@ -1,43 +1,24 @@ import 'reflect-metadata'; -import { createConnection, Connection } from 'typeorm'; -import { ENV } from '@config/environment.config'; -import { Logger } from '@services/logger.service'; +import { DataSource, MixedList } from 'typeorm'; +import { TYPEORM } from '@config/environment.config'; /** * Typeorm default configuration * * @see https://http://typeorm.io */ -export class Database { - - constructor () { } - - /** - * @description Connect to MySQL server - * @async - */ - static connect(options: Record): void { - createConnection({ - type: options.TYPE, - name: options.NAME, - host: options.HOST, - port: options.PORT, - username: options.USER, - password: options.PWD, - database: options.DB, - entities: options.ENTITIES, - subscribers: options.SUBSCRIBERS, - synchronize: options.SYNC, - logging: options.LOG, - cache: options.CACHE - } as any) - .then( (connection: Connection) => { - Logger.log('info', `Connection to MySQL server established on port ${options.PORT as string} (${ENV})`); - }) - .catch( (error: Error) => { - process.stdout.write(`error: ${error.message}`); - process.exit(1); - }); - } -} \ No newline at end of file +export const ApplicationDataSource = new DataSource({ + type: TYPEORM?.TYPE as 'mariadb' | 'mysql', + name: TYPEORM.NAME, + host: TYPEORM.HOST, + port: TYPEORM.PORT, + username: TYPEORM.USER, + password: TYPEORM.PWD, + database: TYPEORM.DB, + entities: TYPEORM.ENTITIES as unknown as MixedList, + subscribers: TYPEORM.SUBSCRIBERS as unknown as MixedList, + synchronize: TYPEORM.SYNC, + logging: TYPEORM.LOG, + cache: TYPEORM.CACHE +}); \ No newline at end of file diff --git a/src/api/config/datasource.config.ts b/src/api/config/datasource.config.ts new file mode 100644 index 0000000..39cb8e5 --- /dev/null +++ b/src/api/config/datasource.config.ts @@ -0,0 +1,35 @@ +import 'reflect-metadata'; + +import { DataSource } from 'typeorm'; + +/** + * Typeorm default configuration used by CLI. This one is the new ormconfig.json + * + * @see https://http://typeorm.io + */ +export const dataSource = new DataSource({ + type: 'mysql', + name: 'default', + host: process.env.CI_DB_HOST || 'localhost', + port: 3306, + username: process.env.CI_DB_USER || 'root', + password: process.env.CI_DB_PWD || 'passw0rd', + database: process.env.CI_DB_NAME || 'typeplate_test', + entities: [ + 'dist/api/core/models/**/*.model.js', + 'dist/api/resources/**/*.model.js' + ], + migrations: [ + 'dist/migrations/**/*.js' + ], + subscribers: [ + 'dist/api/core/subscribers/**/*.subscriber.js', + 'dist/api/resources/**/*.subscriber.js' + ], + synchronize: false, + logging: false, + cache: false, + cli: { + migrationsDir: './dist/migrations' + } +}); \ No newline at end of file diff --git a/src/api/config/environment.config.ts b/src/api/config/environment.config.ts index 37ff744..8bfdab2 100644 --- a/src/api/config/environment.config.ts +++ b/src/api/config/environment.config.ts @@ -34,7 +34,7 @@ export class Environment { /** * @description Current environment */ - environment: string = ENVIRONMENT.development; + environment: ENVIRONMENT = ENVIRONMENT.development; /** * @description Errors staged on current environment @@ -314,7 +314,7 @@ export class Environment { * @default dev */ LOGS_TOKEN: (value: string): string => { - return this.environment === 'production' ? 'combined' : value || 'dev' + return this.environment === ENVIRONMENT.production ? 'combined' : value || 'dev' }, /** @@ -715,7 +715,7 @@ export class Environment { } if ( ( process.argv && process.argv.indexOf('--env') !== -1 ) ) { - this.environment = ENVIRONMENT[process.argv[process.argv.indexOf('--env') + 1]] as string || ENVIRONMENT.development; + this.environment = ENVIRONMENT[process.argv[process.argv.indexOf('--env') + 1]] as ENVIRONMENT || ENVIRONMENT.development; } else if ( process.env.RUNNER ) { this.environment = ENVIRONMENT.test; } else if ( process.env.NODE_ENV ) { diff --git a/src/api/core/controllers/auth.controller.ts b/src/api/core/controllers/auth.controller.ts index 17f43ff..b017c06 100644 --- a/src/api/core/controllers/auth.controller.ts +++ b/src/api/core/controllers/auth.controller.ts @@ -1,9 +1,9 @@ import { Request } from 'express'; -import { getRepository, getCustomRepository } from 'typeorm'; import { badRequest, notFound } from '@hapi/boom'; import * as Jwt from 'jwt-simple'; +import { ApplicationDataSource } from '@config/database.config'; import { ACCESS_TOKEN } from '@config/environment.config'; import { IResponse, IUserRequest, ITokenOptions } from '@interfaces'; import { User } from '@models/user.model'; @@ -44,7 +44,7 @@ class AuthController { */ @Safe() async register(req: Request, res: IResponse): Promise { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); const user = new User(req.body as Record); const count = await repository.count(); if (count === 0) { @@ -63,9 +63,8 @@ class AuthController { */ @Safe() async login(req: Request, res: IResponse): Promise { - const repository = getCustomRepository(UserRepository); - const { user, accessToken } = await repository.findAndGenerateToken(req.body as ITokenOptions); - const token = await AuthService.generateTokenResponse(user, accessToken); + const { user, accessToken } = await UserRepository.findAndGenerateToken(req.body as ITokenOptions); + const token = await AuthService.generateTokenResponse(user as User, accessToken as string); res.locals.data = { token, user }; } @@ -103,8 +102,7 @@ class AuthController { */ @Safe() async refresh(req: Request, res: IResponse, next: (e?: Error) => void): Promise { - const refreshTokenRepository = getRepository(RefreshToken); - const userRepository = getCustomRepository(UserRepository); + const refreshTokenRepository = ApplicationDataSource.getRepository(RefreshToken); const { token } = req.body as { token: { refreshToken?: string } }; @@ -119,8 +117,8 @@ class AuthController { await refreshTokenRepository.remove(refreshToken); // Get owner user of the token - const { user, accessToken } = await userRepository.findAndGenerateToken({ email: refreshToken.user.email , refreshToken }); - const response = await AuthService.generateTokenResponse(user, accessToken); + const { user, accessToken } = await UserRepository.findAndGenerateToken({ email: refreshToken.user.email , refreshToken }); + const response = await AuthService.generateTokenResponse(user as User, accessToken as string); res.locals.data = { token: response }; } @@ -136,14 +134,14 @@ class AuthController { @Safe() async confirm (req: IUserRequest, res: IResponse): Promise { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); const decoded = Jwt.decode(req.body.token, ACCESS_TOKEN.SECRET) as { sub }; if (!decoded) { throw badRequest('User token cannot be read'); } - const user = await repository.findOneOrFail(decoded.sub); + const user = await repository.findOneOrFail(decoded.sub) as User; if ( user.status !== STATUS.REGISTERED && user.status !== STATUS.REVIEWED ) { throw badRequest('User status cannot be confirmed'); @@ -166,9 +164,9 @@ class AuthController { @Safe() async requestPassword (req: IUserRequest, res: IResponse): Promise { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); - const user = await repository.findOne( { email: req.query.email } ); + const user = await repository.findOne( { where: { email: req.query.email } }) as User; if ( user && user.status === STATUS.CONFIRMED ) { void AuthService.revokeRefreshToken(user); diff --git a/src/api/core/controllers/media.controller.ts b/src/api/core/controllers/media.controller.ts index ea1efb1..ec310e6 100644 --- a/src/api/core/controllers/media.controller.ts +++ b/src/api/core/controllers/media.controller.ts @@ -1,10 +1,8 @@ -import { getRepository, getCustomRepository } from 'typeorm'; import { clone } from 'lodash'; -import { IMediaRequest, IResponse } from '@interfaces'; - +import { ApplicationDataSource } from '@config/database.config'; +import { IMedia, IMediaRequest, IResponse } from '@interfaces'; import { Safe } from '@decorators/safe.decorator'; - import { MediaRepository } from '@repositories/media.repository'; import { Media } from '@models/media.model'; @@ -42,8 +40,8 @@ class MediaController { */ @Safe() async get(req: IMediaRequest, res: IResponse): Promise { - const repository = getRepository(Media); - const media = await repository.findOneOrFail(req.params.mediaId, { relations: ['owner'] }); + const repository = ApplicationDataSource.getRepository(Media); + const media = await repository.findOneOrFail({ where: { id: req.params.mediaId }, relations: ['owner'] }) as Media; res.locals.data = media; } @@ -55,12 +53,11 @@ class MediaController { */ @Safe() async list (req: IMediaRequest, res: IResponse): Promise { - const repository = getCustomRepository(MediaRepository); - const response = await repository.list(req.query); + const response = await MediaRepository.list(req.query); res.locals.data = response.result; res.locals.meta = { total: response.total, - pagination: paginate( parseInt(req.query.page, 10), parseInt(req.query.perPage, 10), response.total ) + pagination: paginate( parseInt(req.query.page, 10), parseInt(req.query.perPage, 10), response.total as number ) } } @@ -74,8 +71,8 @@ class MediaController { */ @Safe() async create(req: IMediaRequest, res: IResponse): Promise { - const repository = getRepository(Media); - const medias = [].concat(req.files).map( (file) => new Media(file)); + const repository = ApplicationDataSource.getRepository(Media); + const medias = [].concat(req.files).map( (file) => new Media(file as IMedia)); await repository.save(medias); res.locals.data = medias; } @@ -90,7 +87,7 @@ class MediaController { */ @Safe() async update(req: IMediaRequest, res: IResponse): Promise { - const repository = getRepository(Media); + const repository = ApplicationDataSource.getRepository(Media); const media = clone(res.locals.data) as Media; repository.merge(media, req.files[0] as unknown); await repository.save(media); @@ -107,7 +104,7 @@ class MediaController { */ @Safe() async remove (req: IMediaRequest, res: IResponse): Promise { - const repository = getRepository(Media); + const repository = ApplicationDataSource.getRepository(Media); const media = clone(res.locals.data) as Media; await repository.remove(media); } diff --git a/src/api/core/controllers/user.controller.ts b/src/api/core/controllers/user.controller.ts index a83cc25..82d3583 100644 --- a/src/api/core/controllers/user.controller.ts +++ b/src/api/core/controllers/user.controller.ts @@ -1,11 +1,11 @@ -import { getRepository, getCustomRepository } from 'typeorm'; -import { forbidden } from '@hapi/boom'; +import { forbidden, notFound } from '@hapi/boom'; import { User } from '@models/user.model'; import { UserRepository } from '@repositories/user.repository'; import { IUserRequest, IResponse } from '@interfaces'; import { Safe } from '@decorators/safe.decorator'; import { paginate } from '@utils/pagination.util'; +import { ApplicationDataSource } from '@config/database.config'; /** * Manage incoming requests for api/{version}/users @@ -37,8 +37,7 @@ class UserController { */ @Safe() async get(req: IUserRequest, res: IResponse): Promise { - const repository = getCustomRepository(UserRepository); - res.locals.data = await repository.one(parseInt(req.params.userId, 10)); + res.locals.data = await UserRepository.one(parseInt(req.params.userId as string, 10)); } /** @@ -60,7 +59,7 @@ class UserController { */ @Safe() async create (req: IUserRequest, res: IResponse): Promise { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); const user = new User(req.body); const savedUser = await repository.save(user); res.locals.data = savedUser; @@ -74,8 +73,8 @@ class UserController { */ @Safe() async update (req: IUserRequest, res: IResponse): Promise { - const repository = getRepository(User); - const user = await repository.findOneOrFail(req.params.userId); + const repository = ApplicationDataSource.getRepository(User); + const user = await repository.findOneOrFail({ where: { id: req.params.userId } }); if (req.body.password && req.body.isUpdatePassword) { const pwdMatch = await user.passwordMatches(req.body.passwordToRevoke); if (!pwdMatch) { @@ -95,8 +94,7 @@ class UserController { */ @Safe() async list (req: IUserRequest, res: IResponse): Promise { - const repository = getCustomRepository(UserRepository); - const response = await repository.list(req.query); + const response = await UserRepository.list(req.query); res.locals.data = response.result; res.locals.meta = { total: response.total, @@ -112,8 +110,13 @@ class UserController { */ @Safe() async remove (req: IUserRequest, res: IResponse): Promise { - const repository = getRepository(User); - const user = await repository.findOneOrFail(req.params.userId); + const repository = ApplicationDataSource.getRepository(User); + const user = await repository.findOneOrFail({ where: { id: req.params.userId } }); + + if (!user) { + throw notFound('User not found'); + } + void repository.remove(user); } } diff --git a/src/api/core/middlewares/guard.middleware.ts b/src/api/core/middlewares/guard.middleware.ts index 61f74df..be1cbbe 100644 --- a/src/api/core/middlewares/guard.middleware.ts +++ b/src/api/core/middlewares/guard.middleware.ts @@ -54,7 +54,7 @@ class Guard { if (!roles.includes(user.role)) { return next( forbidden('Forbidden area') ); - } else if (user.role !== ROLE.admin && ( req.params.userId && parseInt(req.params.userId, 10) !== user.id ) ) { + } else if (user.role as ROLE !== ROLE.admin && ( req.params.userId && parseInt(req.params.userId, 10) !== user.id ) ) { return next( forbidden('Forbidden area') ); } diff --git a/src/api/core/models/media.model.ts b/src/api/core/models/media.model.ts index 0cd5192..bd07bc1 100644 --- a/src/api/core/models/media.model.ts +++ b/src/api/core/models/media.model.ts @@ -67,7 +67,7 @@ export class Media implements IModel { /** * @param payload Object data to assign */ - constructor(payload: Record) { + constructor(payload: Record) { Object.assign(this, payload); } diff --git a/src/api/core/models/user.model.ts b/src/api/core/models/user.model.ts index ac45cbe..d12a38a 100644 --- a/src/api/core/models/user.model.ts +++ b/src/api/core/models/user.model.ts @@ -94,6 +94,21 @@ export class User implements IModel { Object.assign(this, payload); } + /** + * @description Filter on allowed entity fields + */ + get whitelist(): string[] { + return [ + 'id', + 'username', + 'avatar', + 'email', + 'role', + 'createdAt' , + 'updatedAt' + ] + } + @AfterLoad() storeTemporaryPassword() : void { this.temporaryPassword = this.password; @@ -125,7 +140,7 @@ export class User implements IModel { /** * @description Generate JWT access token */ - token(duration: number = null): string { + token(duration: number = null): string { const payload = { exp: Dayjs().add(duration || ACCESS_TOKEN.DURATION, 'minutes').unix(), iat: Dayjs().unix(), @@ -133,20 +148,4 @@ export class User implements IModel { }; return Jwt.encode(payload, ACCESS_TOKEN.SECRET); } - - /** - * @description Filter on allowed entity fields - */ - get whitelist(): string[] { - return [ - 'id', - 'username', - 'avatar', - 'email', - 'role', - 'createdAt' , - 'updatedAt' - ] - } - } \ No newline at end of file diff --git a/src/api/core/repositories/media.repository.ts b/src/api/core/repositories/media.repository.ts index 1089d6a..881aa82 100644 --- a/src/api/core/repositories/media.repository.ts +++ b/src/api/core/repositories/media.repository.ts @@ -1,24 +1,14 @@ -import { Repository, EntityRepository, getRepository } from 'typeorm'; import { omitBy, isNil } from 'lodash'; import { Media } from '@models/media.model'; import { IMediaQueryString } from '@interfaces'; import { getMimeTypesOfType } from '@utils/string.util'; +import { ApplicationDataSource } from '@config/database.config'; -@EntityRepository(Media) -export class MediaRepository extends Repository { +export const MediaRepository = ApplicationDataSource.getRepository(Media).extend({ - /** */ - constructor() { - super(); - } - - /** - * @description Get a list of files according to current query - */ - async list({ page = 1, perPage = 30, path, fieldname, filename, size, mimetype, owner, type }: IMediaQueryString): Promise<{result: Media[], total: number}> { - - const repository = getRepository(Media); + list: async ({ page = 1, perPage = 30, path, fieldname, filename, size, mimetype, owner, type }: IMediaQueryString): Promise<{result: Media[], total: number}> => { + const repository = ApplicationDataSource.getRepository(Media); const options = omitBy({ path, fieldname, filename, size, mimetype, owner, type }, isNil) as IMediaQueryString; @@ -53,4 +43,4 @@ export class MediaRepository extends Repository { return { result, total } } -} +}); \ No newline at end of file diff --git a/src/api/core/repositories/refresh-token.repository.ts b/src/api/core/repositories/refresh-token.repository.ts index 8b8bb99..287a159 100644 --- a/src/api/core/repositories/refresh-token.repository.ts +++ b/src/api/core/repositories/refresh-token.repository.ts @@ -1,23 +1,17 @@ -import { Repository, EntityRepository } from 'typeorm'; import { User } from '@models/user.model'; import { RefreshToken } from '@models/refresh-token.model'; import { RefreshTokenFactory } from '@factories/refresh-token.factory'; +import { ApplicationDataSource } from '@config/database.config'; -@EntityRepository(RefreshToken) -export class RefreshTokenRepository extends Repository { - - constructor() { - super(); - } - +export const RefreshTokenRepository = ApplicationDataSource.getRepository(RefreshToken).extend({ /** * @description Generate a new refresh token * * @param user */ - generate(user: User): RefreshToken { + generate: (user: User): RefreshToken => { const refreshToken = RefreshTokenFactory.get(user); - void this.save(refreshToken); + void ApplicationDataSource.getRepository(RefreshToken).save(refreshToken); return refreshToken; } -} +}); \ No newline at end of file diff --git a/src/api/core/repositories/user.repository.ts b/src/api/core/repositories/user.repository.ts index 8d15822..b4eb590 100644 --- a/src/api/core/repositories/user.repository.ts +++ b/src/api/core/repositories/user.repository.ts @@ -1,28 +1,22 @@ import * as Dayjs from 'dayjs'; -import { Repository, EntityRepository, getRepository } from 'typeorm'; import { omitBy, isNil } from 'lodash'; import { badRequest, notFound, unauthorized } from '@hapi/boom'; import { User } from '@models/user.model'; import { IRegistrable, ITokenOptions, IUserQueryString } from '@interfaces'; +import { ApplicationDataSource } from '@config/database.config'; -@EntityRepository(User) -export class UserRepository extends Repository { - - constructor() { - super(); - } - +export const UserRepository = ApplicationDataSource.getRepository(User).extend({ /** * @description Get one user * * @param id - The id of user * */ - async one(id: number): Promise { + one: async (id: number): Promise => { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); const options: { id: number } = omitBy({ id }, isNil) as { id: number }; const user = await repository.findOne({ @@ -34,14 +28,14 @@ export class UserRepository extends Repository { } return user; - } + }, /** * @description Get a list of users according to current query parameters */ - async list({ page = 1, perPage = 30, username, email, role, status }: IUserQueryString): Promise<{result: User[], total: number}> { + list: async ({ page = 1, perPage = 30, username, email, role, status }: IUserQueryString): Promise<{result: User[], total: number}> => { - const repository = getRepository(User); + const repository = ApplicationDataSource.getRepository(User); const options = omitBy({ username, email, role, status }, isNil) as IUserQueryString; const query = repository @@ -70,14 +64,14 @@ export class UserRepository extends Repository { .getManyAndCount(); return { result, total }; - } + }, /** * @description Find user by email and try to generate a JWT token * * @param options Payload data */ - async findAndGenerateToken(options: ITokenOptions): Promise<{user: User, accessToken: string}> { + findAndGenerateToken: async (options: ITokenOptions): Promise<{user: User, accessToken: string}> => { const { email, password, refreshToken, apikey } = options; @@ -85,7 +79,7 @@ export class UserRepository extends Repository { throw badRequest('An email or an API key is required to generate a token') } - const user = await this.findOne({ + const user = await ApplicationDataSource.getRepository(User).findOne({ where : email ? { email } : { apikey } }); @@ -98,7 +92,7 @@ export class UserRepository extends Repository { } return { user, accessToken: user.token() }; - } + }, /** * @description Create / save user for oauth connexion @@ -107,11 +101,11 @@ export class UserRepository extends Repository { * * @fixme user should always retrieved from her email address. If not, possible collision on username value */ - async oAuthLogin(options: IRegistrable): Promise { + oAuthLogin: async (options: IRegistrable): Promise => { const { email, username, password } = options; - const userRepository = getRepository(User); + const userRepository = ApplicationDataSource.getRepository(User); let user = await userRepository.findOne({ where: [ { email }, { username } ], @@ -130,7 +124,8 @@ export class UserRepository extends Repository { } user = userRepository.create({ email, password, username }); + user = await userRepository.save(user); - return userRepository.save(user); + return user; } -} +}); \ No newline at end of file diff --git a/src/api/core/services/auth.service.ts b/src/api/core/services/auth.service.ts index f262566..7076320 100644 --- a/src/api/core/services/auth.service.ts +++ b/src/api/core/services/auth.service.ts @@ -1,7 +1,6 @@ import * as Dayjs from 'dayjs'; import { badData } from '@hapi/boom'; -import { getCustomRepository, getRepository } from 'typeorm'; import { ACCESS_TOKEN } from '@config/environment.config'; @@ -14,6 +13,7 @@ import { RefreshToken } from '@models/refresh-token.model'; import { IOauthResponse } from '@interfaces'; import { hash } from '@utils/string.util'; +import { ApplicationDataSource } from '@config/database.config'; /** * @description @@ -51,11 +51,11 @@ class AuthService { return badData('Access token cannot be retrieved'); } const tokenType = 'Bearer'; - const oldToken = await getRepository(RefreshToken).findOne({ where : { user } }); + const oldToken = await ApplicationDataSource.getRepository(RefreshToken).findOne({ where : { user: { id: user.id } } }); if (oldToken) { - await getRepository(RefreshToken).remove(oldToken) + await ApplicationDataSource.getRepository(RefreshToken).remove(oldToken) } - const refreshToken = getCustomRepository(RefreshTokenRepository).generate(user).token; + const refreshToken = RefreshTokenRepository.generate(user).token; const expiresIn = Dayjs().add(ACCESS_TOKEN.DURATION, 'minutes'); return { tokenType, accessToken, refreshToken, expiresIn }; } @@ -71,10 +71,10 @@ class AuthService { return badData('User is not an instance of User'); } - const oldToken = await getRepository(RefreshToken).findOne({ where : { user } }); + const oldToken = await ApplicationDataSource.getRepository(RefreshToken).findOne({ where : { user: { id: user.id } } }); if (oldToken) { - await getRepository(RefreshToken).remove(oldToken) + await ApplicationDataSource.getRepository(RefreshToken).remove(oldToken) } } @@ -98,8 +98,7 @@ class AuthService { picture: profile.photos.slice().shift()?.value, password: hash(email, 16) } - const userRepository = getCustomRepository(UserRepository); - const user = await userRepository.oAuthLogin(iRegistrable); + const user = await UserRepository.oAuthLogin(iRegistrable); return next(null, user); } catch (err) { return next(err, false); @@ -113,14 +112,14 @@ class AuthService { */ async jwt(payload: { sub }, next: (e?: Error, v?: User|boolean) => void): Promise { try { - const userRepository = getRepository(User); - const user = await userRepository.findOne( payload.sub ); + const userRepository = ApplicationDataSource.getRepository(User); + const user = await userRepository.findOne( { where: { id: payload.sub } }); if (user) { return next(null, user); } return next(null, false); - } catch (error) { - return next(error, false); + } catch (err) { + return next(err, false); } } } diff --git a/src/api/core/types/interfaces/request.interface.ts b/src/api/core/types/interfaces/request.interface.ts index 0f898da..ff04aa0 100644 --- a/src/api/core/types/interfaces/request.interface.ts +++ b/src/api/core/types/interfaces/request.interface.ts @@ -6,5 +6,5 @@ import { Request } from 'express'; export interface IRequest extends Request { user?: any; query: Record, - params: Record; + params: Record; } \ No newline at end of file diff --git a/src/templates/business.service.txt b/src/templates/business.service.txt new file mode 100644 index 0000000..b97139a --- /dev/null +++ b/src/templates/business.service.txt @@ -0,0 +1,56 @@ +import { ROLE, STATUS } from '@enums'; + +import { User } from '@models/user.model'; + +import { {{PASCAL_CASE}} } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}.model'; +import { I{{PASCAL_CASE}}Request } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}-request.interface'; + +import { BusinessRule } from '@shared/types/business-rule.type'; +import { BusinessService } from '@shared/services/business.service'; + +/** + * @description + */ + class {{PASCAL_CASE}}BusinessService extends BusinessService { + + /** + * @description + */ + private static instance: {{PASCAL_CASE}}BusinessService; + + /** + * @description + */ + readonly BUSINESS_RULES: BusinessRule[] = [ + { + key: '{{UPPER_CASE}}_CAN_BE_SUBMITTED_BY_CONFIRMED_USER_ONLY', + description: 'A {{LOWER_CASE}} can be submitted by a confirmed user only.', + statusCode: 403, + methods: [ + 'POST' + ], + check: (user: User, entity: {{PASCAL_CASE}}, payload: I{{PASCAL_CASE}}Request): boolean => { + if (user.status !== STATUS.CONFIRMED) { + return false; + } + return true; + } + } + ]; + + constructor() { + super(); + } + + /** + * @description + */ + static get(): {{PASCAL_CASE}}BusinessService { + if (!{{PASCAL_CASE}}BusinessService.instance) { + {{PASCAL_CASE}}BusinessService.instance = new {{PASCAL_CASE}}BusinessService(); + } + return {{PASCAL_CASE}}BusinessService.instance; + } +} + +export { {{PASCAL_CASE}}BusinessService } \ No newline at end of file diff --git a/src/templates/controller.txt b/src/templates/controller.txt new file mode 100644 index 0000000..9f8a35e --- /dev/null +++ b/src/templates/controller.txt @@ -0,0 +1,116 @@ +import { Request } from 'express'; + +import { ApplicationDataSource } from '@config/database.config'; +import { IRequest, IResponse } from '@interfaces'; +import { Safe } from '@decorators/safe.decorator'; +import { paginate } from '@utils/pagination.util'; + +import { {{PASCAL_CASE}}Repository } from '{{REPOSITORY}}'; +import { {{PASCAL_CASE}} } from '{{MODEL}}'; + +/** + * Manage incoming requests for api/{version}/{{LOWER_CASE_PLURAL}} + */ +class {{PASCAL_CASE}}Controller { + + /** + * @description + */ + private static instance: {{PASCAL_CASE}}Controller; + + private constructor() {} + + /** + * @description + */ + static get(): {{PASCAL_CASE}}Controller { + if (!{{PASCAL_CASE}}Controller.instance) { + {{PASCAL_CASE}}Controller.instance = new {{PASCAL_CASE}}Controller(); + } + return {{PASCAL_CASE}}Controller.instance; + } + + /** + * @description Retrieve one {{CAMEL_CASE}} according to :{{CAMEL_CASE}}Id + * + * @param req Express request object derived from http.incomingMessage + * @param res Express response object + * + * @public + */ + @Safe() + async get(req: IRequest, res: IResponse): Promise { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{CAMEL_CASE}} = await repository.findOneOrFail({ where: { id: req.params.{{CAMEL_CASE}}Id } }); + res.locals.data = {{CAMEL_CASE}}; + } + + /** + * @description Retrieve a list of {{CAMEL_CASE_PLURAL}}, according to some parameters + * + * @param req Express request object derived from http.incomingMessage + * @param res Express response object + */ + @Safe() + async list (req: IRequest, res: IResponse): Promise { + const response = await {{PASCAL_CASE}}Repository.list(req.query); + res.locals.data = response.result; + res.locals.meta = { + total: response.total, + pagination: paginate( parseInt(req.query.page, 10), parseInt(req.query.perPage, 10), response.total ) + } + } + + /** + * @description Create a new {{CAMEL_CASE}} + * + * @param req Express request object derived from http.incomingMessage + * @param res Express response object + * + * @public + */ + @Safe() + async create(req: IRequest, res: IResponse): Promise { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{CAMEL_CASE}} = new {{PASCAL_CASE}}(req.body); + const saved = await repository.save({{CAMEL_CASE}}); + res.locals.data = saved; + } + + /** + * @description Update one {{CAMEL_CASE}} according to :{{CAMEL_CASE}}Id + * + * @param req Express request object derived from http.incomingMessage + * @param res Express response object + * + * @public + */ + @Safe() + async update(req: IRequest, res: IResponse): Promise { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{CAMEL_CASE}} = await repository.findOneOrFail({ where: { id: req.params.{{CAMEL_CASE}}Id } }); + if ({{CAMEL_CASE}}) { + await repository.update(req.params.{{CAMEL_CASE}}Id, req.body); + } + res.locals.data = {{CAMEL_CASE}} ? req.body as {{PASCAL_CASE}} : undefined; + } + + /** + * @description Delete one {{CAMEL_CASE}} according to :{{CAMEL_CASE}}Id + * + * @param req Express request object derived from http.incomingMessage + * @param res Express response object + * + * @public + */ + @Safe() + async remove (req: IRequest, res: IResponse): Promise { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{CAMEL_CASE}} = await repository.findOneOrFail({ where: { id: req.params.{{CAMEL_CASE}}Id } }); + void repository.remove({{CAMEL_CASE}}); + } +} + +const {{CAMEL_CASE}}Controller = {{PASCAL_CASE}}Controller.get(); + +export { {{CAMEL_CASE}}Controller as {{PASCAL_CASE}}Controller } \ No newline at end of file diff --git a/src/templates/data-layer.service.txt b/src/templates/data-layer.service.txt new file mode 100644 index 0000000..9403b12 --- /dev/null +++ b/src/templates/data-layer.service.txt @@ -0,0 +1,104 @@ +import { ApplicationDataSource } from '@config/database.config'; +import { {{PASCAL_CASE}}Repository } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}.repository'; +import { {{PASCAL_CASE}} } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}.model'; +import { I{{PASCAL_CASE}}QueryString } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}-query-string.interface'; +import { I{{PASCAL_CASE}}Request } from './{{LOWER_CASE}}-request.interface'; +import { paginate } from '@utils/pagination.util'; + +/** + * @description + */ +class {{PASCAL_CASE}}DataLayerService { + + /** + * @description + */ + private static instance: {{PASCAL_CASE}}DataLayerService; + + private constructor() {} + + /** + * @description + */ + static get(): {{PASCAL_CASE}}DataLayerService { + if (!{{PASCAL_CASE}}DataLayerService.instance) { + {{PASCAL_CASE}}DataLayerService.instance = new {{PASCAL_CASE}}DataLayerService(); + } + return {{PASCAL_CASE}}DataLayerService.instance; + } + + /** + * @description Retrieve one {{LOWER_CASE}} according to :{{LOWER_CASE}}Id + * + * @param {{LOWER_CASE}}Id + * + * @public + */ + async get({{LOWER_CASE}}Id: string): Promise<{{PASCAL_CASE}}> { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{LOWER_CASE}} = await repository.findOneOrFail({{LOWER_CASE}}Id, { relations: ] } ); + return {{LOWER_CASE}}; + } + + /** + * @description Retrieve a list of {{LOWER_CASE_PLURAL}}, according to query parameters + * + * @param query + */ + async list (query: I{{PASCAL_CASE}}QueryString): Promise<{{PASCAL_CASE}}[]> { + const response = await {{PASCAL_CASE}}Repository.list(query); + res.locals.data = response.result; + res.locals.meta = { + total: response.total, + pagination: paginate( parseInt(req.query.page, 10), parseInt(req.query.perPage, 10), response.total ) + } + return { + data: res.locals.data, + meta: res.locals.meta + } + } + + /** + * @description Create a new {{LOWER_CASE}} + * + * @param payload + * + * @public + */ + async create( { body }: I{{PASCAL_CASE}}Request): Promise<{{PASCAL_CASE}}> { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const {{LOWER_CASE}} = new {{PASCAL_CASE}}(body); + const saved = await repository.save({{LOWER_CASE}}); + return saved; + } + + /** + * @description Update one {{LOWER_CASE}} according to :{{LOWER_CASE}}Id + * + * @param {{LOWER_CASE}}Id + * @param payload + * + * @public + */ + async update({{LOWER_CASE}}: {{PASCAL_CASE}}, { body }: I{{PASCAL_CASE}}Request): Promise<{{PASCAL_CASE}}> { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + repository.merge({{LOWER_CASE}}, body); + const saved = await repository.save({{LOWER_CASE}}); + return saved; + } + + /** + * @description Delete one {{LOWER_CASE}} according to :{{LOWER_CASE}}Id + * + * @param {{LOWER_CASE}}Id + * + * @public + */ + async remove ({{LOWER_CASE}}: {{PASCAL_CASE}}): Promise { + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + void repository.remove({{LOWER_CASE}}); + } + +} + +export { {{PASCAL_CASE}}DataLayerService } \ No newline at end of file diff --git a/src/templates/fixture.txt b/src/templates/fixture.txt new file mode 100644 index 0000000..b06c168 --- /dev/null +++ b/src/templates/fixture.txt @@ -0,0 +1,5 @@ +const chance = require('chance').Chance(); + +exports.entity = { + +} \ No newline at end of file diff --git a/src/templates/model.txt b/src/templates/model.txt new file mode 100644 index 0000000..fdc214b --- /dev/null +++ b/src/templates/model.txt @@ -0,0 +1,45 @@ +require('module-alias/register'); + +import * as Moment from 'moment-timezone'; + +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { IModel } from '@interfaces'; + +@Entity() +export class {{PASCAL_CASE}} implements IModel { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + type: Date, + default: Moment( new Date() ).format('YYYY-MM-DD HH:ss') + }) + createdAt; + + @Column({ + type: Date, + default: null + }) + updatedAt; + + @Column({ + type: Date, + default: null + }) + deletedAt; + + /** + * @param payload Object data to assign + */ + constructor(payload: Record) { + Object.assign(this, payload); + } + + /** + * @description Allowed fields + */ + get whitelist(): string[] { + return []; + } +} \ No newline at end of file diff --git a/src/templates/query-string.interface.txt b/src/templates/query-string.interface.txt new file mode 100644 index 0000000..8e79e7f --- /dev/null +++ b/src/templates/query-string.interface.txt @@ -0,0 +1,3 @@ +import { IQueryString } from '@interfaces'; + +export interface I{{PASCAL_CASE}}QueryString extends IQueryString {} \ No newline at end of file diff --git a/src/templates/repository.txt b/src/templates/repository.txt new file mode 100644 index 0000000..f230faf --- /dev/null +++ b/src/templates/repository.txt @@ -0,0 +1,53 @@ +import { omitBy, isNil } from 'lodash'; +import { notFound } from'@hapi/boom'; + +import { ApplicationDataSource } from '@config/database.config'; +import { {{PASCAL_CASE}} } from '{{MODEL}}'; + +export const {{PASCAL_CASE}}Repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}).extend({ + + constructor() { + super(); + } + + /** + * @description Get one {{LOWER_CASE}} + * + * @param id - The id of {{LOWER_CASE}} + * + */ + async one(id: number): Promise<{{PASCAL_CASE}}> { + + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const options: { id: number } = omitBy({ id }, isNil) as { id: number }; + + const {{CAMEL_CASE}} = await repository.findOne({ + where: options + }); + + if (!{{CAMEL_CASE}}) { + throw notFound('{{PASCAL_CASE}} not found'); + } + + return {{CAMEL_CASE}}; + } + + /** + * Get a list of {{LOWER_CASE}}s according to current query parameters + * + * @public + */ + async list({ page = 1, perPage = 30 }: { page: number, perPage: number }): Promise<{{PASCAL_CASE}}[]> { + + const repository = ApplicationDataSource.getRepository({{PASCAL_CASE}}); + const options = {}; /** @todo omitBy({}, isNil) **/ + + const [ result, total ] = await repository.find({ + where: options, + skip: ( page - 1 ) * perPage, + take: perPage + }); + + return { result, total }; + } +}) \ No newline at end of file diff --git a/src/templates/request.interface.txt b/src/templates/request.interface.txt new file mode 100644 index 0000000..189ab61 --- /dev/null +++ b/src/templates/request.interface.txt @@ -0,0 +1,8 @@ +import { IRequest, IMedia } from '@interfaces'; + +/** + * @description + */ +export interface I{{PASCAL_CASE}}Request extends IRequest { + body: {} +} \ No newline at end of file diff --git a/src/templates/route.txt b/src/templates/route.txt new file mode 100644 index 0000000..f75da85 --- /dev/null +++ b/src/templates/route.txt @@ -0,0 +1,116 @@ +import { {{PASCAL_CASE}}Controller } from '{{CONTROLLER}}'; +import { Router } from '@classes/router.class'; +import { Guard } from '@middlewares/guard.middleware'; +import { Validator } from '@middlewares/validator.middleware'; +import { ROLE } from '@enums'; +import { list{{PASCAL_CASE_PLURAL}}, insert{{PASCAL_CASE}}, get{{PASCAL_CASE}}, replace{{PASCAL_CASE}}, update{{PASCAL_CASE}}, remove{{PASCAL_CASE}} } from '{{VALIDATION}}'; + +export class {{PASCAL_CASE}}Router extends Router { + + constructor(){ + super(); + } + + /** + * @description Plug routes definitions + */ + define(): void { + + this.router.route('/') + + /** + * @api {get} api/v1/{{HYPHEN_PLURAL}} List {{CAMEL_CASE_PLURAL}} + * @apiDescription Get a list of {{HYPHEN_PLURAL}} + * @apiVersion 1.0.0 + * @apiName List{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission ROLE.admin + * + * @apiUse BaseHeader + * + * @apiParam {Number{1-}} [page=1] List page + * @apiParam {Number{1-100}} [perPage=1] {{PASCAL_CASE}}'s per page + * + * TODO: + */ + .get(Guard.authorize([{{PERMISSIONS}}]), Validator.check(list{{PASCAL_CASE_PLURAL}}), {{PASCAL_CASE}}Controller.list) + + /** + * @api {post} api/v1/{{HYPHEN_PLURAL}} Create {{CAMEL_CASE_PLURAL}} + * @apiDescription Create one or many new {{CAMEL_CASE_PLURAL}} + * @apiVersion 1.0.0 + * @apiName Create{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission user + * + * @apiUse BaseHeader + * + * TODO: + */ + .post(Guard.authorize([{{PERMISSIONS}}]), Validator.check(insert{{PASCAL_CASE}}), {{PASCAL_CASE}}Controller.create); + + this.router.route('/:{{CAMEL_CASE}}Id') + + /** + * @api {get} api/v1/{{HYPHEN_PLURAL}}/:id Get one {{CAMEL_CASE}} + * @apiDescription Get {{CAMEL_CASE}} + * @apiVersion 1.0.0 + * @apiName Get{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission user + * + * @apiUse BaseHeader + * + * TODO: + */ + .get(Guard.authorize([{{PERMISSIONS}}]), Validator.check(get{{PASCAL_CASE}}), {{PASCAL_CASE}}Controller.get) + + /** + * @api {put} api/v1/{{HYPHEN_PLURAL}}/:id Replace {{CAMEL_CASE}} + * @apiDescription Replace the whole {{CAMEL_CASE}} with a new one + * @apiVersion 1.0.0 + * @apiName Replace{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission user + * + * @apiUse BaseHeader + * + * TODO: + */ + .put(Guard.authorize([{{PERMISSIONS}}]), Validator.check(replace{{PASCAL_CASE}}), {{PASCAL_CASE}}Controller.update) + + /** + * @api {patch} api/v1/{{HYPHEN_PLURAL}}/:id Update {{CAMEL_CASE}} + * @apiDescription Update some fields of a {{CAMEL_CASE}} + * @apiVersion 1.0.0 + * @apiName Update{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission user + * + * @apiUse BaseHeader + * + * TODO: + */ + .patch(Guard.authorize([{{PERMISSIONS}}]), Validator.check(update{{PASCAL_CASE}}), {{PASCAL_CASE}}Controller.update) + + /** + * @api {patch} api/v1/{{HYPHEN_PLURAL}}/:id Delete {{CAMEL_CASE}} + * @apiDescription Delete a {{CAMEL_CASE}} + * @apiVersion 1.0.0 + * @apiName Delete{{PASCAL_CASE}} + * @apiGroup {{PASCAL_CASE}} + * @apiPermission user + * + * @apiUse BaseHeader + * + * @apiError (Bad request 400) ValidationError Some parameters may contain invalid values + * @apiError (Unauthorized 401) Unauthorized Only authenticated users can access the data + * @apiError (Forbidden 403) Forbidden Only ROLE.admins can access the data + * @apiError (Not Found 404) NotFound {{PASCAL_CASE}} does not exist + * + * TODO: + */ + .delete(Guard.authorize([{{PERMISSIONS}}]), Validator.check(remove{{PASCAL_CASE}}), {{PASCAL_CASE}}Controller.remove); + + } +} \ No newline at end of file diff --git a/src/templates/subscriber.txt b/src/templates/subscriber.txt new file mode 100644 index 0000000..6b29bac --- /dev/null +++ b/src/templates/subscriber.txt @@ -0,0 +1,50 @@ +require('module-alias/register'); + +import * as Dayjs from 'dayjs'; + +import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm'; +import { {{PASCAL_CASE}} } from '@resources/{{LOWER_CASE}}/{{LOWER_CASE}}.model'; + +/** + * + */ +@EventSubscriber() +export class {{PASCAL_CASE}}Subscriber implements EntitySubscriberInterface<{{PASCAL_CASE}}> { + + /** + * @description Indicates that this subscriber only listen to {{PASCAL_CASE}} events. + */ + listenTo(): any { + return {{PASCAL_CASE}}; + } + + /** + * @description Called before {{PASCAL_CASE}} insertion. + */ + beforeInsert(event: InsertEvent<{{PASCAL_CASE}}>): void { + event.entity.createdAt = Dayjs( new Date() ).toDate(); + } + + /** + * @description Called after {{PASCAL_CASE}} insertion. + */ + afterInsert(event: InsertEvent<{{PASCAL_CASE}}>): void {} + + /** + * @description Called before {{PASCAL_CASE}} update. + */ + beforeUpdate(event: UpdateEvent<{{PASCAL_CASE}}>): void { + event.entity.updatedAt = Dayjs( new Date() ).toDate(); + } + + /** + * @description Called after {{PASCAL_CASE}} update. + */ + afterUpdate(event: UpdateEvent<{{PASCAL_CASE}}>): void {} + + /** + * @description Called after {{PASCAL_CASE}} deletion. + */ + afterRemove(event: RemoveEvent<{{PASCAL_CASE}}>): void {} + +} \ No newline at end of file diff --git a/src/templates/test.txt b/src/templates/test.txt new file mode 100644 index 0000000..d478505 --- /dev/null +++ b/src/templates/test.txt @@ -0,0 +1,130 @@ +const request = require('supertest'); +const { expect } = require('chai'); +const { clone } = require('lodash'); + +let { server } = require(process.cwd() + '/dist/api/app.bootstrap'); + +const { user, {{CAMEL_CASE}} } = require(process.cwd() + '/test/utils/fixtures'); +const { doRequest, doQueryRequest } = require(process.cwd() + '/test/utils'); + +describe("{{PASCAL_CASE}} routes", function () { + + let agent, token, unauthorizedToken, _{{CAMEL_CASE}}; + + before(function (done) { + + agent = request(server); + + doRequest(agent, 'post', '/api/v1/auth/register', null, null, user.entity('admin', 'e2q2mak7'), function(err, res) { + token = res.body.token.accessToken; + doRequest(agent, 'post', '/api/v1/auth/register', null, null, user.entity('user', 'e2q2mak7'), function(err, res) { + unauthorizedToken = res.body.token.accessToken; + done(); + }); + }); + + }); + + after(function () { + server.close(); + delete server; + }); + + describe('POST /api/v1/{{HYPHEN_PLURAL}}', () => { + + it('201 - succeed', function (done) { + const params = clone({{CAMEL_CASE}}); + doRequest(agent, 'post', '/api/v1/{{HYPHEN_PLURAL}}', null, null, params, function(err, res) { + expect(res.statusCode).to.eqls(201); + _{{CAMEL_CASE}} = res.body; + done(); + }); + }); + + }); + + describe('GET /api/v1/{{HYPHEN_PLURAL}}', () => { + + it('200 - ok', function (done) { + doQueryRequest(agent, '/api/v1/{{HYPHEN_PLURAL}}', null, null, {}, function(err, res) { + expect(res.statusCode).to.eqls(200); + done(); + }); + }); + + }); + + describe('GET /api/v1/{{HYPHEN_PLURAL}}/:id', () => { + + it('200 - ok', function (done) { + doQueryRequest(agent, `/api/v1/{{HYPHEN_PLURAL}}/`, _{{CAMEL_CASE}}.id, null, {}, function(err, res) { + expect(res.statusCode).to.eqls(200); + done(); + }); + }); + + }); + + describe('PUT /api/v1/{{HYPHEN_PLURAL}}/:id', () => { + + it('404 - not found', function (done) { + const params = clone({{CAMEL_CASE}}); + doRequest(agent, 'put', '/api/v1/{{HYPHEN_PLURAL}}/', 2569, token, params, function(err, res) { + expect(res.statusCode).to.eqls(404); + done(); + }); + }); + + it('200 - ok', function (done) { + const params = clone({{CAMEL_CASE}}); + doRequest(agent, 'put', '/api/v1/{{HYPHEN_PLURAL}}/', _{{CAMEL_CASE}}.id, token, params, function(err, res) { + expect(res.statusCode).to.eqls(200); + done(); + }); + }); + + }); + + describe('PATCH /api/v1/{{HYPHEN_PLURAL}}/:id', () => { + + it('404 - not found', function (done) { + const params = clone({{CAMEL_CASE}}); + doRequest(agent, 'patch', '/api/v1/{{HYPHEN_PLURAL}}/', 2569, token, params, function(err, res) { + expect(res.statusCode).to.eqls(404); + done(); + }); + }); + + it('200 - ok', function (done) { + const params = clone({{CAMEL_CASE}}); + doRequest(agent, 'patch', '/api/v1/{{HYPHEN_PLURAL}}/', _{{CAMEL_CASE}}.id, null, params, function(err, res) { + expect(res.statusCode).to.eqls(200); + done(); + }); + }); + + }); + + describe('DELETE /api/v1/{{HYPHEN_PLURAL}}/:id', () => { + + it('404 - not found', function (done) { + agent + .delete('/api/v1/{{HYPHEN_PLURAL}}/' + 2754) + .set('Authorization', 'Bearer ' + token) + .set('Origin', process.env.ORIGIN) + .set('Content-Type', process.env.CONTENT_TYPE) + .expect(404, done); + }); + + it('204', function (done) { + agent + .delete('/api/v1/{{HYPHEN_PLURAL}}/' + _{{CAMEL_CASE}}.id) + .set('Authorization', 'Bearer ' + token) + .set('Origin', process.env.ORIGIN) + .set('Content-Type', process.env.CONTENT_TYPE) + .expect(204, done); + }); + + }); + +}); \ No newline at end of file diff --git a/src/templates/validation.txt b/src/templates/validation.txt new file mode 100644 index 0000000..e0cae7b --- /dev/null +++ b/src/templates/validation.txt @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import * as Joi from 'joi'; + +// GET /v1/{{HYPHEN_PLURAL}} +const list{{PASCAL_CASE_PLURAL}} = { + query: Joi.object({ + filter: Joi.number().min(0).max(1), + page: Joi.number().min(1), + perPage: Joi.number().min(1).max(100), + }) +}; + +// GET /v1/{{HYPHEN_PLURAL}}/:{{CAMEL_CASE}}Id +const get{{PASCAL_CASE}} = { + params: Joi.object({ + {{CAMEL_CASE}}Id: Joi.string().regex(/^[0-9]{1,4}$/).required() + }) +}; + +// POST /v1/{{HYPHEN_PLURAL}} +const insert{{PASCAL_CASE}} = { + body: Joi.object({}) +}; + +// PUT /v1/{{HYPHEN_PLURAL}}/:{{CAMEL_CASE}}Id +const replace{{PASCAL_CASE}} = { + body: Joi.object({}), + params: Joi.object({ + {{CAMEL_CASE}}Id: Joi.string().regex(/^[0-9]{1,4}$/).required(), + }) +}; + +// PATCH /v1/{{HYPHEN_PLURAL}}/:{{CAMEL_CASE}}Id +const update{{PASCAL_CASE}} = { + body: Joi.object({}), + params: Joi.object({ + {{CAMEL_CASE}}Id: Joi.string().regex(/^[0-9]{1,4}$/).required(), + }) +}; + +// DELETE /v1/{{HYPHEN_PLURAL}}/:{{CAMEL_CASE}}Id +const remove{{PASCAL_CASE}} = { + body: Joi.object({}), + params: Joi.object({ + {{CAMEL_CASE}}Id: Joi.string().regex(/^[0-9]{1,4}$/).required(), + }) +}; + +export { list{{PASCAL_CASE_PLURAL}}, get{{PASCAL_CASE}}, insert{{PASCAL_CASE}}, replace{{PASCAL_CASE}}, update{{PASCAL_CASE}}, remove{{PASCAL_CASE}} }; \ No newline at end of file diff --git a/test/units/00-application.unit.test.js b/test/units/00-application.unit.test.js index d5b3e99..1a1d989 100644 --- a/test/units/00-application.unit.test.js +++ b/test/units/00-application.unit.test.js @@ -2,7 +2,7 @@ let { server } = require(process.cwd() + '/dist/api/app.bootstrap'); describe('Units tests', () => { - before( () => {}); + before( () => {} ); after( () => { server.close(); diff --git a/test/units/01-express-app.unit.test.js b/test/units/01-express-app.unit.test.js index 7455f16..c0c51ea 100644 --- a/test/units/01-express-app.unit.test.js +++ b/test/units/01-express-app.unit.test.js @@ -9,7 +9,7 @@ describe('Express application', () => { expect(typeof(application)).to.equal('function'); }); - it('Express server version is 4.18.3', () => { + it('Express server version is 4.18.2', () => { expect(pkgInfo.dependencies.express).to.equal('4.18.2'); }); diff --git a/test/units/02-config.unit.test.js b/test/units/02-config.unit.test.js index f6b93ac..c9325b7 100644 --- a/test/units/02-config.unit.test.js +++ b/test/units/02-config.unit.test.js @@ -5,7 +5,7 @@ const fs = require('fs'); const { clone } = require('lodash'); const { Environment, TYPEORM } = require(process.cwd() + '/dist/api/config/environment.config'); -const { Database } = require(process.cwd() + '/dist/api/config/database.config'); +const { ApplicationDataSource } = require(process.cwd() + '/dist/api/config/database.config'); const { LoggerConfiguration } = require(process.cwd() + '/dist/api/config/logger.config'); describe('Config', function () { @@ -160,7 +160,10 @@ describe('Config', function () { done(); }); - Database.connect(options); + ApplicationDataSource.initialize().then(() => {}).catch((error) => { + process.stdout.write(`error: ${error.message}`); + process.exit(1); + }) }) })