From 81da0f2766e49cd0cc53462981fe5a2d52ab9275 Mon Sep 17 00:00:00 2001 From: Inga Date: Sun, 12 Jan 2025 21:00:18 +0000 Subject: [PATCH] backend service structure, config, DB connection --- service/.eslintrc.js | 4 +- service/package-lock.json | 393 ++++++++++++++++++++++++++++- service/package.json | 6 + service/src/app.controller.spec.ts | 17 +- service/src/app.controller.ts | 2 +- service/src/app.module.ts | 17 +- service/src/app.service.ts | 12 +- service/src/db/index.ts | 32 +++ service/src/main.ts | 2 + service/src/utils/nestjsHelpers.ts | 13 + service/test/app.e2e-spec.ts | 10 +- 11 files changed, 493 insertions(+), 15 deletions(-) create mode 100644 service/src/db/index.ts create mode 100644 service/src/utils/nestjsHelpers.ts diff --git a/service/.eslintrc.js b/service/.eslintrc.js index 93d0495..a7742b6 100644 --- a/service/.eslintrc.js +++ b/service/.eslintrc.js @@ -18,5 +18,7 @@ module.exports = { jest: true, }, ignorePatterns: ['.eslintrc.js'], - rules: {}, + rules: { + "@typescript-eslint/restrict-template-expressions": ["error", { "allowNumber": true, "allowNever": true }] + }, }; diff --git a/service/package-lock.json b/service/package-lock.json index 3b6f8c5..0739702 100644 --- a/service/package-lock.json +++ b/service/package-lock.json @@ -10,8 +10,13 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "pg": "^8.13.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -23,6 +28,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/pg": "^8.11.10", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -1562,6 +1568,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -1651,6 +1663,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.15.tgz", @@ -1689,6 +1716,26 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", + "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz", @@ -1734,6 +1781,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@nestjs/swagger": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.1.tgz", + "integrity": "sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.6", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.18.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.15.tgz", @@ -1842,6 +1922,13 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2118,6 +2205,80 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/pg": { + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", @@ -2186,6 +2347,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2783,7 +2950,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-flatten": { @@ -3363,6 +3529,23 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -3876,6 +4059,27 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6285,7 +6489,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6412,6 +6615,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz", + "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -6449,7 +6658,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { @@ -6871,6 +7079,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7128,6 +7343,105 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7237,6 +7551,52 @@ "node": ">=4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8071,6 +8431,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8311,6 +8680,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -8952,6 +9330,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/service/package.json b/service/package.json index 765104a..964f380 100644 --- a/service/package.json +++ b/service/package.json @@ -21,8 +21,13 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "pg": "^8.13.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -34,6 +39,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/pg": "^8.11.10", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/service/src/app.controller.spec.ts b/service/src/app.controller.spec.ts index d22f389..54a1537 100644 --- a/service/src/app.controller.spec.ts +++ b/service/src/app.controller.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { DbClient } from './db'; describe('AppController', () => { let appController: AppController; @@ -8,15 +9,25 @@ describe('AppController', () => { beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], - providers: [AppService], + providers: [ + { + provide: 'dbClient', + useValue: { + getNumberOfAllSlots: () => Promise.resolve(123), + } satisfies DbClient, + }, + AppService, + ], }).compile(); appController = app.get(AppController); }); describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); + it('should return "Hello World!"', async () => { + await expect(appController.getHello()).resolves.toBe( + 'Hello World! There are 123 total slots.', + ); }); }); }); diff --git a/service/src/app.controller.ts b/service/src/app.controller.ts index cce879e..70918a7 100644 --- a/service/src/app.controller.ts +++ b/service/src/app.controller.ts @@ -6,7 +6,7 @@ export class AppController { constructor(private readonly appService: AppService) {} @Get() - getHello(): string { + async getHello(): Promise { return this.appService.getHello(); } } diff --git a/service/src/app.module.ts b/service/src/app.module.ts index e0d67c3..0d05ae0 100644 --- a/service/src/app.module.ts +++ b/service/src/app.module.ts @@ -1,11 +1,24 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { createDbClient } from './db'; @Module({ - imports: [], + imports: [ConfigModule.forRoot({ ignoreEnvFile: true })], controllers: [AppController], - providers: [AppService], + providers: [ + { + provide: 'dbClient', + useFactory: (configService: ConfigService) => + createDbClient( + configService.get('PG_CONNECTION_STRING') ?? + 'postgres://postgres:mypassword123!@localhost:5432/coding-challenge', + ), + inject: [ConfigService], + }, + AppService, + ], }) // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class AppModule {} diff --git a/service/src/app.service.ts b/service/src/app.service.ts index 927d7cc..e5397d0 100644 --- a/service/src/app.service.ts +++ b/service/src/app.service.ts @@ -1,8 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DbClient } from './db'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; + constructor( + @Inject('dbClient') + private readonly dbClient: DbClient, + ) {} + + async getHello(): Promise { + return `Hello World! There are ${await this.dbClient.getNumberOfAllSlots()} total slots.`; } } diff --git a/service/src/db/index.ts b/service/src/db/index.ts new file mode 100644 index 0000000..fecaced --- /dev/null +++ b/service/src/db/index.ts @@ -0,0 +1,32 @@ +import { OnModuleDestroy } from '@nestjs/common'; +import { Pool } from 'pg'; + +export interface DbClient { + getNumberOfAllSlots(): Promise; +} + +export const createDbClient = async ( + connectionString: string, +): Promise => { + const pool = new Pool({ connectionString }); + + { + // Checking DB connectivity + const connection = await pool.connect(); + connection.release(); + } + + return { + getNumberOfAllSlots: async () => { + const result = await pool.query<{ count: string }>({ + text: 'select count(*) as count from slots', + }); + + return parseInt(result.rows[0]?.count ?? '0'); + }, + + onModuleDestroy: async () => { + await pool.end(); + }, + }; +}; diff --git a/service/src/main.ts b/service/src/main.ts index 3534114..ab17341 100644 --- a/service/src/main.ts +++ b/service/src/main.ts @@ -1,8 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { configureApp } from './utils/nestjsHelpers'; async function bootstrap() { const app = await NestFactory.create(AppModule); + configureApp(app); await app.listen(process.env['PORT'] ?? 3000); } void bootstrap(); diff --git a/service/src/utils/nestjsHelpers.ts b/service/src/utils/nestjsHelpers.ts new file mode 100644 index 0000000..95ae630 --- /dev/null +++ b/service/src/utils/nestjsHelpers.ts @@ -0,0 +1,13 @@ +import { type INestApplication, ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export const configureApp = (app: INestApplication) => { + app.useGlobalPipes(new ValidationPipe({ transform: true })); + + const config = new DocumentBuilder() + .setTitle('Test joyn assignment') + .setVersion('1.0') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); +}; diff --git a/service/test/app.e2e-spec.ts b/service/test/app.e2e-spec.ts index 3eba382..18dcab1 100644 --- a/service/test/app.e2e-spec.ts +++ b/service/test/app.e2e-spec.ts @@ -2,7 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import request from 'supertest'; import type { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; +import { AppModule } from '../src/app.module'; +import { configureApp } from '../src/utils/nestjsHelpers'; describe('AppController (e2e)', () => { let app: INestApplication; @@ -13,13 +14,18 @@ describe('AppController (e2e)', () => { }).compile(); app = moduleFixture.createNestApplication>(); + configureApp(app); await app.init(); }); + afterEach(async () => { + await app.close(); + }); + it('/ (GET)', () => { return request(app.getHttpServer()) .get('/') .expect(200) - .expect('Hello World!'); + .expect('Hello World! There are 18 total slots.'); }); });