From 68e0db7c0976680b888157ca2cd1c42b1d762651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej?= <46246339+BartekCK@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:02:28 +0100 Subject: [PATCH] feat: :sparkles: create dexcom infrastructure service (#5) * feat: :sparkles: add dexcom auth * feat: :sparkles: add dexcom routes * feat: :sparkles: add common command handler --- .env.example | 2 + .env.test | 2 + .eslintrc.json | 27 +- .github/workflows/test.yml | 6 + .gitignore | 2 + README.md | 2 +- jest.config.ts | 3 + package-lock.json | 579 ++++++++++++++++++ package.json | 7 +- .../createReminderHandler.changetotest.ts | 132 ++++ .../synchroniseLatestReadingsCommand.ts | 19 + ...synchroniseLatestReadingsCommandHandler.ts | 68 ++ .../envConfig/envDexcomConfig.interface.ts | 4 - .../dexcomService/dexcomReading.interface.ts | 10 + .../dexcomService/dexcomService.interface.ts | 14 + src/common/__tests__/asserations.ts | 2 +- src/common/command-bus/command.interface.ts | 7 + .../command-bus/commandBus.interface.ts | 8 + src/common/command-bus/commandBus.ts | 46 ++ .../command-bus/commandHandler.interface.ts | 9 + src/common/command-bus/index.ts | 4 + src/common/types/databaseFailure.ts | 9 +- src/common/types/result.ts | 25 +- src/common/types/userLocation.ts | 4 + src/domain/entities/cgmGlucose/cgmGlucose.ts | 6 +- .../envConfig/envConfig.interface.ts | 2 +- src/infrastructure/envConfig/envConfig.ts | 13 + ... => cgmGlucoseDbEntityMapper.unit.test.ts} | 0 ... cgmGlucoseRepository.integration.test.ts} | 1 + .../cgmGlucoseRepository.ts | 10 +- .../__tests__/dexcomEntityTestFactory.ts | 20 + .../dexcomService.integration.test.ts | 218 +++++++ .../auth/dexcomAuth.interface.ts | 32 + .../services/dexcomService/auth/dexcomAuth.ts | 116 ++++ .../dexcomService/dexcomEntity.inteface.ts | 25 + .../services/dexcomService/dexcomService.ts | 188 ++++++ .../envDexcomConfig.interface.ts | 8 + .../__tests__/dexcomEntityMapper.unit.test.ts | 28 + .../mappers/dexcomEntityMapper.interface.ts | 6 + .../mappers/dexcomEntityMapper.ts | 19 + .../routing/dexcomAuthRoute.inteface.ts | 4 + .../routing/dexcomRoute.interface.ts | 4 + .../dexcomService/routing/dexcomRoutes.ts | 26 + .../routing/dexcomRoutes.unit.test.ts | 80 +++ .../routing/dexcomServiceRoute.inteface.ts | 3 + 45 files changed, 1753 insertions(+), 47 deletions(-) create mode 100644 src/application/commands/synchroniseLatestReadings/__tests__/createReminderHandler.changetotest.ts create mode 100644 src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommand.ts create mode 100644 src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommandHandler.ts delete mode 100644 src/application/config/envConfig/envDexcomConfig.interface.ts create mode 100644 src/application/services/dexcomService/dexcomReading.interface.ts create mode 100644 src/application/services/dexcomService/dexcomService.interface.ts create mode 100644 src/common/command-bus/command.interface.ts create mode 100644 src/common/command-bus/commandBus.interface.ts create mode 100644 src/common/command-bus/commandBus.ts create mode 100644 src/common/command-bus/commandHandler.interface.ts create mode 100644 src/common/command-bus/index.ts create mode 100644 src/common/types/userLocation.ts rename src/infrastructure/repositories/cgmGlucoseRepository/__tests__/{cgmGlucoseDbEntityMapper.test.ts => cgmGlucoseDbEntityMapper.unit.test.ts} (100%) rename src/infrastructure/repositories/cgmGlucoseRepository/__tests__/{cgmGlucoseRepository.test.ts => cgmGlucoseRepository.integration.test.ts} (98%) create mode 100644 src/infrastructure/services/dexcomService/__tests__/dexcomEntityTestFactory.ts create mode 100644 src/infrastructure/services/dexcomService/__tests__/dexcomService.integration.test.ts create mode 100644 src/infrastructure/services/dexcomService/auth/dexcomAuth.interface.ts create mode 100644 src/infrastructure/services/dexcomService/auth/dexcomAuth.ts create mode 100644 src/infrastructure/services/dexcomService/dexcomEntity.inteface.ts create mode 100644 src/infrastructure/services/dexcomService/dexcomService.ts create mode 100644 src/infrastructure/services/dexcomService/envDexcomConfig.interface.ts create mode 100644 src/infrastructure/services/dexcomService/mappers/__tests__/dexcomEntityMapper.unit.test.ts create mode 100644 src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.interface.ts create mode 100644 src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.ts create mode 100644 src/infrastructure/services/dexcomService/routing/dexcomAuthRoute.inteface.ts create mode 100644 src/infrastructure/services/dexcomService/routing/dexcomRoute.interface.ts create mode 100644 src/infrastructure/services/dexcomService/routing/dexcomRoutes.ts create mode 100644 src/infrastructure/services/dexcomService/routing/dexcomRoutes.unit.test.ts create mode 100644 src/infrastructure/services/dexcomService/routing/dexcomServiceRoute.inteface.ts diff --git a/.env.example b/.env.example index 1db4900..02c10f0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ DEXCOM_USERNAME=exampleUsername DEXCOM_PASSWORD=examplePassword +DEXCOM_APPLICATION_ID=d89443d2-327c-4a6f-89e5-496bbb0317db +DEXCOM_USER_LOCATION=EU DATABASE_HOST=exampleHost DATABASE_PORT=8080 diff --git a/.env.test b/.env.test index 288ab9c..23fb664 100644 --- a/.env.test +++ b/.env.test @@ -1,5 +1,7 @@ DEXCOM_USERNAME=test DEXCOM_PASSWORD=test +DEXCOM_APPLICATION_ID=d89443d2-327c-4a6f-89e5-496bbb0317db +DEXCOM_USER_LOCATION=EU DATABASE_HOST=127.0.0.1 DATABASE_PORT=5432 diff --git a/.eslintrc.json b/.eslintrc.json index e04e163..9474f38 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,32 +8,17 @@ "eslint:recommended", "plugin:@typescript-eslint/recommended" ], - "overrides": [ - ], + "overrides": [], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest" }, - "plugins": [ - "@typescript-eslint" - ], + "plugins": ["@typescript-eslint"], "rules": { - "indent": [ - "error", - 4 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ] + "indent": ["error", 4], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "always"] }, "ignorePatterns": ["dist", "node_modules"] } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b6a1db..008d908 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,12 @@ jobs: - name: Run test run: npm test + - name: Upload coverage reports + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov + build: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 794e9e0..8b3bdf6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules dist/ .idea .env +.coverage +coverage diff --git a/README.md b/README.md index 25ba8ee..2162749 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ### TODO: -1. Exclude dir migrations and tests from production build +1. Add contract tests ### Migrations diff --git a/jest.config.ts b/jest.config.ts index 0174310..85d6c20 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,7 +3,10 @@ import type { Config } from 'jest'; const config: Config = { verbose: true, preset: 'ts-jest', + testPathIgnorePatterns: ['/node_modules/'], testMatch: ['**/*.test.ts'], + collectCoverage: true, + coverageDirectory: '.coverage', }; export default config; diff --git a/package-lock.json b/package-lock.json index 960b454..8d08887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.2.3", "knex": "^2.4.1", "pg": "^8.8.0", "uuid": "^9.0.0", @@ -25,6 +26,8 @@ "eslint": "^8.32.0", "eslint-config-prettier": "^8.6.0", "jest": "^29.3.1", + "nock": "^13.3.0", + "nodemon": "^2.0.20", "prettier": "^2.8.3", "rimraf": "^4.1.1", "ts-jest": "^29.0.5", @@ -1605,6 +1608,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/acorn": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", @@ -1736,6 +1745,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz", + "integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", @@ -1833,6 +1857,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1977,6 +2010,45 @@ "node": ">=10" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", @@ -2051,6 +2123,17 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -2128,6 +2211,14 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2664,6 +2755,38 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2855,6 +2978,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2929,6 +3058,18 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -3706,6 +3847,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3924,6 +4071,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3962,6 +4128,21 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nock": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.0.tgz", + "integrity": "sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3974,6 +4155,88 @@ "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", "dev": true }, + "node_modules/nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4414,6 +4677,26 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/punycode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", @@ -4449,6 +4732,18 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -4626,6 +4921,27 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -4850,6 +5166,18 @@ "node": ">=8.0" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/ts-jest": { "version": "29.0.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", @@ -5003,6 +5331,12 @@ "node": ">=4.2.0" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", @@ -6451,6 +6785,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "acorn": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", @@ -6542,6 +6882,21 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz", + "integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "babel-jest": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", @@ -6618,6 +6973,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6712,6 +7073,33 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "ci-info": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", @@ -6767,6 +7155,14 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -6827,6 +7223,11 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -7231,6 +7632,21 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7367,6 +7783,12 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -7420,6 +7842,15 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-core-module": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", @@ -8012,6 +8443,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -8163,6 +8600,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8195,6 +8645,18 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "nock": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.0.tgz", + "integrity": "sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8207,6 +8669,65 @@ "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", "dev": true }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8518,6 +9039,23 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "punycode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.2.0.tgz", @@ -8536,6 +9074,15 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -8646,6 +9193,23 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -8812,6 +9376,15 @@ "is-number": "^7.0.0" } }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, "ts-jest": { "version": "29.0.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", @@ -8891,6 +9464,12 @@ "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", diff --git a/package.json b/package.json index b07f39e..670ec71 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "", "main": "index.js", "scripts": { - "dev": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon src/main.ts", "build": "rimraf ./dist && tsc", "lint": "eslint \"src/**/*.ts\" --fix --quiet", "check-types": "tsc --noEmit", "pretest": "NODE_ENV=test npm run migrations:latest", - "test": "NODE_ENV=test jest --runInBand", + "test": "NODE_ENV=test jest --runInBand --config jest.config.ts", "posttest": "NODE_ENV=test npm run migrations:rollback:all", "migrations:create:dev": "NODE_ENV=development knex migrate:make -x ts", "migrations:latest": "knex migrate:latest", @@ -30,6 +30,8 @@ "eslint": "^8.32.0", "eslint-config-prettier": "^8.6.0", "jest": "^29.3.1", + "nock": "^13.3.0", + "nodemon": "^2.0.20", "prettier": "^2.8.3", "rimraf": "^4.1.1", "ts-jest": "^29.0.5", @@ -37,6 +39,7 @@ "typescript": "^4.9.4" }, "dependencies": { + "axios": "^1.2.3", "knex": "^2.4.1", "pg": "^8.8.0", "uuid": "^9.0.0", diff --git a/src/application/commands/synchroniseLatestReadings/__tests__/createReminderHandler.changetotest.ts b/src/application/commands/synchroniseLatestReadings/__tests__/createReminderHandler.changetotest.ts new file mode 100644 index 0000000..d0a8e58 --- /dev/null +++ b/src/application/commands/synchroniseLatestReadings/__tests__/createReminderHandler.changetotest.ts @@ -0,0 +1,132 @@ +// import { +// CreateReminderCommandResult, +// CreateReminderCommandSuccess, +// } from '../synchroniseLatestReadingsCommandHandler'; +// import { DependencyInjector } from '../../../../../core/dependency-injector'; +// import { ICommandBus } from '../../../../../common/command-bus'; +// import { SynchroniseLatestReadingsCommand } from '../synchroniseLatestReadingsCommand'; +// import { faker } from '@faker-js/faker'; +// import { v4 } from 'uuid'; +// import { IDatabaseClient } from '../../../../../common/database'; +// import { IReminderRepository } from '../../../repositories'; +// import { assertFailure } from '../../../../../common/tests/assertFailure'; +// import { +// InvalidPayloadFailure, +// OutcomeFailure, +// } from '../../../../../common/error-handling'; +// +// describe('Create reminder handler', () => { +// let tableName: string; +// +// let commandBus: ICommandBus; +// let databaseClient: IDatabaseClient; +// let reminderRepository: IReminderRepository; +// +// beforeAll(async () => { +// await DependencyInjector.create(); +// const { +// commandBus: depCommandBus, +// databaseClient: depDatabaseClient, +// environmentLocalStore: depEnvService, +// reminderRepository: depReminderRepository, +// } = DependencyInjector.getDependencies(); +// commandBus = depCommandBus; +// databaseClient = depDatabaseClient; +// reminderRepository = depReminderRepository; +// tableName = depEnvService.getEventsTableName(); +// +// await databaseClient.createEventTable(tableName); +// }); +// +// afterAll(async () => { +// await databaseClient.deleteEventTable(tableName); +// databaseClient.destroy(); +// }); +// +// describe('Given correct `CreateReminderCommand`', () => { +// const command = new SynchroniseLatestReadingsCommand({ +// note: faker.lorem.slug(), +// plannedExecutionDate: faker.date.future(), +// userId: v4(), +// traceId: v4(), +// }); +// +// describe('when command is executed', () => { +// let result: CreateReminderCommandSuccess; +// +// beforeAll(async () => { +// const handlerResult = await commandBus.execute< +// SynchroniseLatestReadingsCommand, +// Promise +// >(command); +// +// if (handlerResult.isFailure()) { +// throw new Error('Should return success'); +// } +// +// result = handlerResult; +// }); +// +// it('then should result be success', () => { +// expect(result.isSuccess()).toBeTruthy(); +// expect(result.getData()).toEqual(null); +// }); +// +// it('then should save `CreateReminderDomainEvent` in database', async () => { +// const { Items } = await databaseClient.scan({ TableName: tableName }); +// +// if (!Items || !Items[0]) { +// throw new Error('Items should contain first element'); +// } +// +// expect(Items[0]).toEqual( +// expect.objectContaining({ +// name: 'CreateReminderDomainEvent', +// sequence: 1, +// }), +// ); +// }); +// }); +// }); +// +// describe('Given incorrect `CreateReminderCommand`', () => { +// const command = new SynchroniseLatestReadingsCommand({ +// note: faker.lorem.slug(), +// plannedExecutionDate: faker.date.future(), +// userId: 'INCORRECT_USER_ID', +// traceId: v4(), +// }); +// +// describe('when command is executed', () => { +// let result: OutcomeFailure; +// +// beforeAll(async () => { +// jest.spyOn(reminderRepository, 'save'); +// +// const handlerResult = await commandBus.execute< +// SynchroniseLatestReadingsCommand, +// Promise +// >(command); +// +// assertFailure(handlerResult); +// +// result = handlerResult; +// }); +// +// it('then result should be failure', () => { +// expect(result.isFailure()).toBeTruthy(); +// +// expect(result.getError().errorScope).toEqual('DOMAIN_ERROR'); +// expect(result.getError().errorCode).toEqual('INCORRECT_COMMAND_PAYLOAD'); +// }); +// +// it('then failure should be InvalidPayloadFailure', () => { +// expect(result).toBeInstanceOf(InvalidPayloadFailure); +// }); +// +// it('then repository should not be call', () => { +// expect(reminderRepository.save).not.toBeCalled(); +// }); +// }); +// }); +// }); diff --git a/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommand.ts b/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommand.ts new file mode 100644 index 0000000..0723dba --- /dev/null +++ b/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommand.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; +import { commandSchema, ICommand } from '../../../common/command-bus'; + +export const synchroniseLatestReadingsCommandPayloadSchema = commandSchema.extend({}); + +export type ISynchroniseLatestReadingsCommand = z.infer< + typeof synchroniseLatestReadingsCommandPayloadSchema +> & + ICommand; + +export class SynchroniseLatestReadingsCommand +implements ISynchroniseLatestReadingsCommand +{ + readonly traceId: string; + + constructor({ traceId }: ISynchroniseLatestReadingsCommand) { + this.traceId = traceId; + } +} diff --git a/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommandHandler.ts b/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommandHandler.ts new file mode 100644 index 0000000..dc33662 --- /dev/null +++ b/src/application/commands/synchroniseLatestReadings/synchroniseLatestReadingsCommandHandler.ts @@ -0,0 +1,68 @@ +import { + ISynchroniseLatestReadingsCommand, + synchroniseLatestReadingsCommandPayloadSchema, +} from './synchroniseLatestReadingsCommand'; +import { SuccessResult } from '../../../common/types/result'; +import { + CgmGlucose, + CgmGlucoseId, + CgmGlucoseTrend, +} from '../../../domain/entities/cgmGlucose/cgmGlucose'; +import { DatabaseFailure } from '../../../common/types/databaseFailure'; +import { ICgmGlucoseRepository } from '../../repositories/cgmGlucoseRepository/cgmGlucoseRepository.interface'; +import { ICommand, ICommandHandler } from '../../../common/command-bus'; + +export class SynchroniseLatestReadingsCommandHandlerSuccess extends SuccessResult<{ + id: CgmGlucoseId; +}> {} +export type SynchroniseLatestReadingsCommandHandlerFailure = DatabaseFailure; + +export type SynchroniseLatestReadingsCommandHandlerResult = + | SynchroniseLatestReadingsCommandHandlerSuccess + | SynchroniseLatestReadingsCommandHandlerFailure; + +type Dependencies = { + cgmGlucoseRepository: ICgmGlucoseRepository; +}; + +export class SynchroniseLatestReadingsCommandHandler +implements + ICommandHandler< + ISynchroniseLatestReadingsCommand, + Promise + > +{ + private readonly cgmGlucoseRepository: ICgmGlucoseRepository; + + constructor(dependencies: Dependencies) { + this.cgmGlucoseRepository = dependencies.cgmGlucoseRepository; + } + + async handle( + command: ICommand, + ): Promise { + const validationResult = + synchroniseLatestReadingsCommandPayloadSchema.safeParse(command); + + if (!validationResult.success) { + throw new Error('Not implemented yet'); + // return InvalidPayloadFailure.create(valiasddationResult.error); + } + + const cgmGlucose = CgmGlucose.create({ + trend: CgmGlucoseTrend.SingleUp, + value: 123, + valueDate: new Date(), + }); + + const saveResult = await this.cgmGlucoseRepository.save(cgmGlucose); + + if (saveResult.isFailure()) { + return saveResult; + } + + return new SynchroniseLatestReadingsCommandHandlerSuccess({ + id: cgmGlucose.getState().id, + }); + } +} diff --git a/src/application/config/envConfig/envDexcomConfig.interface.ts b/src/application/config/envConfig/envDexcomConfig.interface.ts deleted file mode 100644 index 3340442..0000000 --- a/src/application/config/envConfig/envDexcomConfig.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IEnvDexcomConfig { - getDexcomUsername: () => string; - getDexcomPassword: () => string; -} diff --git a/src/application/services/dexcomService/dexcomReading.interface.ts b/src/application/services/dexcomService/dexcomReading.interface.ts new file mode 100644 index 0000000..9f49343 --- /dev/null +++ b/src/application/services/dexcomService/dexcomReading.interface.ts @@ -0,0 +1,10 @@ +import { CgmGlucoseTrend } from '../../../domain/entities/cgmGlucose/cgmGlucose'; +import { z } from 'zod'; + +export const dexcomReadingSchema = z.object({ + value: z.number().optional(), + valueDate: z.date(), + trend: z.nativeEnum(CgmGlucoseTrend), +}); + +export type IDexcomReading = z.infer; diff --git a/src/application/services/dexcomService/dexcomService.interface.ts b/src/application/services/dexcomService/dexcomService.interface.ts new file mode 100644 index 0000000..4aeba65 --- /dev/null +++ b/src/application/services/dexcomService/dexcomService.interface.ts @@ -0,0 +1,14 @@ +import { FailureResult, SuccessResult } from '../../../common/types/result'; +import { IDexcomReading } from './dexcomReading.interface'; + +export class GetReadingsSuccess extends SuccessResult<{ readings: IDexcomReading[] }> {} +export class GetReadingsFailure extends FailureResult {} + +export type GetReadingsResult = GetReadingsSuccess | GetReadingsFailure; + +export interface IDexcomService { + getReadings: (data: { + minutesBefore: number; + maxCount: number; + }) => Promise; +} diff --git a/src/common/__tests__/asserations.ts b/src/common/__tests__/asserations.ts index cbe01e3..d5c557c 100644 --- a/src/common/__tests__/asserations.ts +++ b/src/common/__tests__/asserations.ts @@ -12,7 +12,7 @@ export function assertResultSuccess( export function assertResultFailure( result: FailureResult | SuccessResult, -): asserts result is SuccessResult { +): asserts result is FailureResult { if (result.isSuccess()) { throw new Error('Result should be failure'); } diff --git a/src/common/command-bus/command.interface.ts b/src/common/command-bus/command.interface.ts new file mode 100644 index 0000000..37ecf59 --- /dev/null +++ b/src/common/command-bus/command.interface.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const commandSchema = z.object({ + traceId: z.string(), +}); + +export type ICommand = z.infer; diff --git a/src/common/command-bus/commandBus.interface.ts b/src/common/command-bus/commandBus.interface.ts new file mode 100644 index 0000000..e77acdb --- /dev/null +++ b/src/common/command-bus/commandBus.interface.ts @@ -0,0 +1,8 @@ +import { ICommand } from './command.interface'; +import { Result } from '../types/result'; + +export interface ICommandBus { + execute: >( + command: Command, + ) => R; +} diff --git a/src/common/command-bus/commandBus.ts b/src/common/command-bus/commandBus.ts new file mode 100644 index 0000000..5b23e94 --- /dev/null +++ b/src/common/command-bus/commandBus.ts @@ -0,0 +1,46 @@ +import { ICommandBus } from './commandBus.interface'; +import { ICommandHandler } from './commandHandler.interface'; +import { ICommand } from './command.interface'; +import { Result } from '../types/result'; + +export class CommandBus implements ICommandBus { + private commandHandlers: Map< + string, + ICommandHandler> + >; + + constructor( + commandHandlers: Map>>, + ) { + this.commandHandlers = commandHandlers; + } + + public execute>( + command: Command, + ): R { + const handler = this.findCommandHandler(command); + + if (!handler) { + throw new Error( + `Command for handler doesn't exist, commandName: ${command.constructor.name}`, + { + cause: { + commandName: command.constructor.name, + }, + }, + ); + } + + return handler.handle(command); + } + + private findCommandHandler( + command: ICommand, + ): ICommandHandler> | null { + const className = command.constructor.name; + + const handler = this.commandHandlers.get(className); + + return handler || null; + } +} diff --git a/src/common/command-bus/commandHandler.interface.ts b/src/common/command-bus/commandHandler.interface.ts new file mode 100644 index 0000000..dd22a2c --- /dev/null +++ b/src/common/command-bus/commandHandler.interface.ts @@ -0,0 +1,9 @@ +import { ICommand } from './command.interface'; +import { Result } from '../types/result'; + +export interface ICommandHandler< + Command extends ICommand, + R extends Result | Promise, +> { + handle: (command: Command) => R; +} diff --git a/src/common/command-bus/index.ts b/src/common/command-bus/index.ts new file mode 100644 index 0000000..5cd9713 --- /dev/null +++ b/src/common/command-bus/index.ts @@ -0,0 +1,4 @@ +export * from './commandBus.interface'; +export * from './commandBus'; +export * from './command.interface'; +export * from './commandHandler.interface'; diff --git a/src/common/types/databaseFailure.ts b/src/common/types/databaseFailure.ts index 5ad9010..c0b4d85 100644 --- a/src/common/types/databaseFailure.ts +++ b/src/common/types/databaseFailure.ts @@ -1,7 +1,12 @@ import { FailureResult } from './result'; export class DatabaseFailure extends FailureResult { - public constructor(errorMessage: string, context?: unknown) { - super(errorMessage, 'DATABASE_FAILURE', 'INFRASTRUCTURE_ERROR', context); + public constructor(data: { errorMessage: string; context?: unknown }) { + super({ + errorMessage: data.errorMessage, + errorCode: 'DATABASE_FAILURE', + errorType: 'INFRASTRUCTURE_ERROR', + context: data.context, + }); } } diff --git a/src/common/types/result.ts b/src/common/types/result.ts index 06bfdc0..aafeaf4 100644 --- a/src/common/types/result.ts +++ b/src/common/types/result.ts @@ -31,18 +31,21 @@ export abstract class SuccessResult extends Result { } } +export type ERROR_TYPE = 'DOMAIN_ERROR' | 'INFRASTRUCTURE_ERROR'; + export abstract class FailureResult extends Result { protected readonly errorMessage: string; protected readonly errorCode: string; - protected readonly errorType: 'DOMAIN_ERROR' | 'INFRASTRUCTURE_ERROR'; + protected readonly errorType: ERROR_TYPE; protected readonly context?: T; - public constructor( - errorMessage: string, - errorCode: string, - errorType: 'DOMAIN_ERROR' | 'INFRASTRUCTURE_ERROR', - context?: T, - ) { + public constructor(data: { + errorMessage: string; + errorCode: string; + errorType: 'DOMAIN_ERROR' | 'INFRASTRUCTURE_ERROR'; + context?: T; + }) { + const { errorCode, errorType, errorMessage, context } = data; super(false); this.errorMessage = errorMessage; @@ -51,11 +54,17 @@ export abstract class FailureResult extends Result { this.context = context; } - public getError(): { errorMessage: string; errorCode: string; errorType: string } { + public getError(): { + errorMessage: string; + errorCode: string; + errorType: string; + context?: T; + } { return { errorMessage: this.errorMessage, errorCode: this.errorCode, errorType: this.errorType, + context: this.context, }; } diff --git a/src/common/types/userLocation.ts b/src/common/types/userLocation.ts new file mode 100644 index 0000000..e66ddb0 --- /dev/null +++ b/src/common/types/userLocation.ts @@ -0,0 +1,4 @@ +export enum UserLocation { + US = 'US', + EU = 'EU', +} diff --git a/src/domain/entities/cgmGlucose/cgmGlucose.ts b/src/domain/entities/cgmGlucose/cgmGlucose.ts index cfda77a..5555917 100644 --- a/src/domain/entities/cgmGlucose/cgmGlucose.ts +++ b/src/domain/entities/cgmGlucose/cgmGlucose.ts @@ -45,11 +45,7 @@ export class CgmGlucose { }: CreateCgmGlucosePayload): CgmGlucose { if ( !value && - ![ - CgmGlucoseTrend.RateOutOfRange, - CgmGlucoseTrend.NotComputable, - CgmGlucoseTrend.None, - ].includes(trend) + ![CgmGlucoseTrend.RateOutOfRange, CgmGlucoseTrend.None].includes(trend) ) { throw new Error('Should be value here'); } diff --git a/src/infrastructure/envConfig/envConfig.interface.ts b/src/infrastructure/envConfig/envConfig.interface.ts index fa7150d..f864c1b 100644 --- a/src/infrastructure/envConfig/envConfig.interface.ts +++ b/src/infrastructure/envConfig/envConfig.interface.ts @@ -1,4 +1,4 @@ -import { IEnvDexcomConfig } from '../../application/config/envConfig/envDexcomConfig.interface'; +import { IEnvDexcomConfig } from '../services/dexcomService/envDexcomConfig.interface'; import { IDbClientEnvConfig } from '../database/client/dbClientEnvConfig.inteface'; export interface IEnvConfig extends IEnvDexcomConfig, IDbClientEnvConfig {} diff --git a/src/infrastructure/envConfig/envConfig.ts b/src/infrastructure/envConfig/envConfig.ts index 4dfcb96..38f4315 100644 --- a/src/infrastructure/envConfig/envConfig.ts +++ b/src/infrastructure/envConfig/envConfig.ts @@ -1,9 +1,12 @@ import { z } from 'zod'; import { IEnvConfig } from './envConfig.interface'; +import { UserLocation } from '../../common/types/userLocation'; const configPropsSchema = z.object({ dexcomPassword: z.string(), dexcomUsername: z.string(), + dexcomApplicationId: z.string(), + dexcomUserLocation: z.nativeEnum(UserLocation), databaseHost: z.string(), databasePort: z.number(), databaseUser: z.string(), @@ -20,6 +23,8 @@ export class EnvConfig implements IEnvConfig { const props = configPropsSchema.parse({ dexcomPassword: process.env['DEXCOM_PASSWORD'], dexcomUsername: process.env['DEXCOM_USERNAME'], + dexcomApplicationId: process.env['DEXCOM_APPLICATION_ID'], + dexcomUserLocation: process.env['DEXCOM_USER_LOCATION'], databaseHost: process.env['DATABASE_HOST'], databasePort: Number(process.env['DATABASE_PORT']), databaseUser: process.env['DATABASE_USER'], @@ -30,6 +35,10 @@ export class EnvConfig implements IEnvConfig { this.props = props; } + getDexcomUserLocation(): UserLocation { + return this.props.dexcomUserLocation; + } + public static async factory(): Promise { if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') { const { config } = await import('dotenv'); @@ -69,4 +78,8 @@ export class EnvConfig implements IEnvConfig { getDatabaseUser(): string { return this.props.databaseUser; } + + getDexcomApplicationId(): string { + return this.props.dexcomApplicationId; + } } diff --git a/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseDbEntityMapper.test.ts b/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseDbEntityMapper.unit.test.ts similarity index 100% rename from src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseDbEntityMapper.test.ts rename to src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseDbEntityMapper.unit.test.ts diff --git a/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.test.ts b/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.integration.test.ts similarity index 98% rename from src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.test.ts rename to src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.integration.test.ts index 6d01e2c..28200e0 100644 --- a/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.test.ts +++ b/src/infrastructure/repositories/cgmGlucoseRepository/__tests__/cgmGlucoseRepository.integration.test.ts @@ -122,6 +122,7 @@ describe('cgmGlucoseRepository', () => { }); beforeAll(async () => { + await dbClient(CGM_GLUCOSE_TABLE_NAME).truncate(); await dbClient .insert([pastCgmGlucoseDbEntity, latestCgmGlucoseDbEntity]) .into(CGM_GLUCOSE_TABLE_NAME); diff --git a/src/infrastructure/repositories/cgmGlucoseRepository/cgmGlucoseRepository.ts b/src/infrastructure/repositories/cgmGlucoseRepository/cgmGlucoseRepository.ts index 3f59ca7..648c89d 100644 --- a/src/infrastructure/repositories/cgmGlucoseRepository/cgmGlucoseRepository.ts +++ b/src/infrastructure/repositories/cgmGlucoseRepository/cgmGlucoseRepository.ts @@ -35,7 +35,10 @@ export class CgmGlucoseRepository implements ICgmGlucoseRepository { this.cgmGlucoseDbEntityMapper.mapIntoCgmGlucoseEntity(result), ); } catch (error) { - return new DatabaseFailure('GetLatestReadingSuccessResult', error); + return new DatabaseFailure({ + errorMessage: 'Error occurred during getting latest CgmGlucose item', + context: { error }, + }); } } @@ -52,7 +55,10 @@ export class CgmGlucoseRepository implements ICgmGlucoseRepository { return new SaveSuccess({ id: glucose.getState().id }); } catch (error) { - return new DatabaseFailure('SaveResult', error); + return new DatabaseFailure({ + errorMessage: 'Error occurred during save CgmGlucose', + context: { error }, + }); } } } diff --git a/src/infrastructure/services/dexcomService/__tests__/dexcomEntityTestFactory.ts b/src/infrastructure/services/dexcomService/__tests__/dexcomEntityTestFactory.ts new file mode 100644 index 0000000..20fac17 --- /dev/null +++ b/src/infrastructure/services/dexcomService/__tests__/dexcomEntityTestFactory.ts @@ -0,0 +1,20 @@ +import { IDexcomEntity } from '../dexcomEntity.inteface'; +import { faker } from '@faker-js/faker'; + +type DexcomEntityTestFactoryInput = Omit, 'ST' | 'DT' | 'WT'> & { + valueDate?: Date; +}; + +export const dexcomEntityTestFactory = ( + input?: DexcomEntityTestFactoryInput, +): IDexcomEntity => { + const randomDate = input?.valueDate || faker.date.past(); + + return { + WT: `Date(${randomDate.getTime()})`, + ST: `Date(${randomDate.getTime()})`, + DT: `Date(${randomDate.getTime()}+0100)`, + Value: input?.Value || 115, + Trend: input?.Trend || 'Flat', + }; +}; diff --git a/src/infrastructure/services/dexcomService/__tests__/dexcomService.integration.test.ts b/src/infrastructure/services/dexcomService/__tests__/dexcomService.integration.test.ts new file mode 100644 index 0000000..5ac42f8 --- /dev/null +++ b/src/infrastructure/services/dexcomService/__tests__/dexcomService.integration.test.ts @@ -0,0 +1,218 @@ +import { + GetReadingsResult, + GetReadingsSuccess, + IDexcomService, +} from '../../../../application/services/dexcomService/dexcomService.interface'; +import { DexcomService } from '../dexcomService'; +import { IDexcomAuth } from '../auth/dexcomAuth.interface'; +import { DexcomAuth } from '../auth/dexcomAuth'; +import { IDexcomRoute } from '../routing/dexcomRoute.interface'; +import { DexcomRoute } from '../routing/dexcomRoutes'; +import { IEnvDexcomConfig } from '../envDexcomConfig.interface'; +import { EnvConfig } from '../../../envConfig/envConfig'; +import { IDexcomEntityMapper } from '../mappers/dexcomEntityMapper.interface'; +import { DexcomEntityMapper } from '../mappers/dexcomEntityMapper'; +import { faker } from '@faker-js/faker'; +import { + assertResultFailure, + assertResultSuccess, +} from '../../../../common/__tests__/asserations'; +import nock = require('nock'); +import { dexcomEntityTestFactory } from './dexcomEntityTestFactory'; + +describe('DexcomService', () => { + let dexcomService: IDexcomService; + let dexcomRoute: IDexcomRoute; + + const minutesBefore = 10; + const maxCount = 10; + const sessionId = faker.datatype.uuid(); + + beforeAll(async () => { + const dexcomConfig: IEnvDexcomConfig = await EnvConfig.factory(); + dexcomRoute = new DexcomRoute(dexcomConfig); + const dexcomAuth: IDexcomAuth = new DexcomAuth(dexcomRoute, dexcomConfig); + const dexcomEntityMapper: IDexcomEntityMapper = new DexcomEntityMapper(); + + nock(dexcomRoute.getAuthUrl()).persist(true).post('').reply(200, { + accountId: faker.datatype.uuid(), + }); + + nock(dexcomRoute.getLoginUrl()).persist(true).post('').reply(200, { + sessionId, + }); + + dexcomService = new DexcomService(dexcomAuth, dexcomRoute, dexcomEntityMapper); + }); + + describe('Given incorrect input', () => { + describe('when minutesBefore are < 1 ', () => { + it('then should return failure', async () => { + const result = await dexcomService.getReadings({ + minutesBefore: 0, + maxCount: 100, + }); + assertResultFailure(result); + }); + }); + + describe('when minutesBefore are > 1440 ', () => { + it('then should return failure', async () => { + const result = await dexcomService.getReadings({ + minutesBefore: 1441, + maxCount: 100, + }); + assertResultFailure(result); + }); + }); + + describe('when maxCount is < 1 ', () => { + it('then should return failure', async () => { + const result = await dexcomService.getReadings({ + minutesBefore: 10, + maxCount: 0, + }); + assertResultFailure(result); + }); + }); + + describe('when maxCount is > 287 ', () => { + it('then should return failure', async () => { + const result = await dexcomService.getReadings({ + minutesBefore: 10, + maxCount: 288, + }); + assertResultFailure(result); + }); + }); + }); + + describe('Given correct input', () => { + describe('when http request return unknown error', () => { + let result: GetReadingsResult; + + beforeAll(async () => { + nock(dexcomRoute.getLatestGlucoseReadingsUrl()) + .post('', {}) + .reply(500, {}); + + result = await dexcomService.getReadings({ minutesBefore, maxCount }); + }); + + it('then should return failure', async () => { + assertResultFailure(result); + + expect(result.getError()).toEqual({ + errorMessage: 'Something went wrong during request', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'HTTP_REQUEST_ERROR', + context: expect.any(Object), + }); + }); + }); + + describe('when data from http request return unknown schema', () => { + let result: GetReadingsResult; + + beforeAll(async () => { + nock(dexcomRoute.getLatestGlucoseReadingsUrl(), {}) + .post( + `?sessionID[sessionId]=${sessionId}&minutes=${minutesBefore}&maxCount=${maxCount}`, + {}, + ) + .reply(200, [ + { ...dexcomEntityTestFactory(), DT: 'INCORRECT_VALUE' }, + ]); + + result = await dexcomService.getReadings({ minutesBefore, maxCount }); + }); + + it('then should return failure', async () => { + assertResultFailure(result); + + expect(result.getError()).toEqual({ + errorMessage: 'Schema not match', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'UNKNOWN_SCHEMA_ERROR', + context: expect.any(Object), + }); + }); + }); + + describe('when first call finish with expired sessionId but second is ok', () => { + let result: GetReadingsSuccess; + + const valueDate = faker.date.past(); + const dexcomEntity = dexcomEntityTestFactory({ valueDate }); + + beforeAll(async () => { + nock(dexcomRoute.getLatestGlucoseReadingsUrl(), {}) + .post( + `?sessionID[sessionId]=${sessionId}&minutes=${minutesBefore}&maxCount=${maxCount}`, + {}, + ) + .reply(500, { code: 'SessionNotValid' }) + .post( + `?sessionID[sessionId]=${sessionId}&minutes=${minutesBefore}&maxCount=${maxCount}`, + {}, + ) + .reply(200, [dexcomEntity]); + + result = (await dexcomService.getReadings({ + minutesBefore, + maxCount, + })) as GetReadingsSuccess; + }); + + it('then should return success', async () => { + assertResultSuccess(result); + }); + + it('then should result is mapped into readings', () => { + expect(result.getData()).toStrictEqual({ + readings: [ + { + value: dexcomEntity.Value, + trend: dexcomEntity.Trend, + valueDate, + }, + ], + }); + }); + }); + + describe('when first and "n" call finish with expired sessionId', () => { + let result: GetReadingsResult; + + beforeAll(async () => { + nock(dexcomRoute.getLatestGlucoseReadingsUrl(), {}) + .post( + `?sessionID[sessionId]=${sessionId}&minutes=${minutesBefore}&maxCount=${maxCount}`, + {}, + ) + .reply(500, { code: 'SessionNotValid' }) + .post( + `?sessionID[sessionId]=${sessionId}&minutes=${minutesBefore}&maxCount=${maxCount}`, + {}, + ) + .reply(500, { code: 'SessionNotValid' }); + + result = await dexcomService.getReadings({ + minutesBefore, + maxCount, + }); + }); + + it('then should return failure', async () => { + assertResultFailure(result); + + expect(result.getError()).toStrictEqual({ + errorMessage: 'Session id is expired', + errorCode: 'SESSION_ID_EXPIRED', + errorType: 'INFRASTRUCTURE_ERROR', + context: { data: { code: 'SessionNotValid' } }, + }); + }); + }); + }); +}); diff --git a/src/infrastructure/services/dexcomService/auth/dexcomAuth.interface.ts b/src/infrastructure/services/dexcomService/auth/dexcomAuth.interface.ts new file mode 100644 index 0000000..8b73a0a --- /dev/null +++ b/src/infrastructure/services/dexcomService/auth/dexcomAuth.interface.ts @@ -0,0 +1,32 @@ +import { IDexcomAuthState } from './dexcomAuth'; +import { + ERROR_TYPE, + FailureResult, + SuccessResult, +} from '../../../../common/types/result'; + +export class CreateAuthStateSuccess extends SuccessResult<{ auth: IDexcomAuthState }> {} + +export type CreateAuthStateErrorCode = + | 'HTTP_REQUEST_ERROR' + | 'UNKNOWN_LIBRARY_ERROR' + | 'INVALID_AUTH_CREDENTIALS'; +export class CreateAuthStateFailure extends FailureResult { + protected readonly errorCode: CreateAuthStateErrorCode; + + constructor(data: { + errorMessage: string; + errorCode: CreateAuthStateErrorCode; + errorType: ERROR_TYPE; + context?: any; + }) { + super(data); + this.errorCode = data.errorCode; + } +} + +export type CreateAuthStateResult = CreateAuthStateSuccess | CreateAuthStateFailure; + +export interface IDexcomAuth { + createAuthState: () => Promise; +} diff --git a/src/infrastructure/services/dexcomService/auth/dexcomAuth.ts b/src/infrastructure/services/dexcomService/auth/dexcomAuth.ts new file mode 100644 index 0000000..95cdaf5 --- /dev/null +++ b/src/infrastructure/services/dexcomService/auth/dexcomAuth.ts @@ -0,0 +1,116 @@ +import axios, { AxiosError } from 'axios'; +import { IEnvDexcomConfig } from '../envDexcomConfig.interface'; +import { + CreateAuthStateFailure, + CreateAuthStateResult, + CreateAuthStateSuccess, + IDexcomAuth, +} from './dexcomAuth.interface'; +import { IDexcomRoute } from '../routing/dexcomRoute.interface'; + +export interface IDexcomAuthState { + sessionId: string; +} + +export class DexcomAuth implements IDexcomAuth { + constructor( + private readonly dexcomRoute: IDexcomRoute, + private readonly envDexcomConfig: IEnvDexcomConfig, + ) {} + + public async createAuthState(): Promise { + const accountIdResult = await this.auth(); + + if (accountIdResult instanceof CreateAuthStateFailure) { + return accountIdResult; + } + + const accountId: string = accountIdResult; + + const sessionIdResult = await this.login(accountId); + + if (sessionIdResult instanceof CreateAuthStateFailure) { + return sessionIdResult; + } + + const sessionId: string = sessionIdResult; + + return new CreateAuthStateSuccess({ auth: { sessionId } }); + } + + private async auth(): Promise { + try { + const response = await axios.post(this.dexcomRoute.getAuthUrl(), { + accountName: this.envDexcomConfig.getDexcomUsername(), + password: this.envDexcomConfig.getDexcomPassword(), + applicationId: this.envDexcomConfig.getDexcomApplicationId(), + }); + + const accountId = response.data; + + return accountId; + } catch (error) { + return this.handleError(error); + } + } + + private async login(accountId: string): Promise { + try { + const response = await axios.post(this.dexcomRoute.getLoginUrl(), { + password: this.envDexcomConfig.getDexcomPassword(), + applicationId: this.envDexcomConfig.getDexcomApplicationId(), + accountId, + }); + + const sessionId = response.data; + + return sessionId; + } catch (error) { + return this.handleError(error); + } + } + + private handleError(errorInstance: Error | unknown): CreateAuthStateFailure { + if (!(errorInstance instanceof AxiosError)) { + return new CreateAuthStateFailure({ + errorMessage: 'Something went wrong during axios post request', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'UNKNOWN_LIBRARY_ERROR', + context: { + library: 'axios', + method: 'auth', + class: DexcomAuth.name, + }, + }); + } + + if (errorInstance.response?.data.code === 'AccountPasswordInvalid') { + return new CreateAuthStateFailure({ + errorMessage: 'Invalid dexcom credentials', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'INVALID_AUTH_CREDENTIALS', + context: { + data: errorInstance.response.data, + }, + }); + } + + if (errorInstance.response?.data.code === 'InvalidArgument') { + return new CreateAuthStateFailure({ + errorMessage: 'Invalid auth argument', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'INVALID_AUTH_CREDENTIALS', + context: { + data: errorInstance.response.data, + }, + }); + } + + return new CreateAuthStateFailure({ + errorMessage: 'Something went wrong during request', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'HTTP_REQUEST_ERROR', + context: { ...errorInstance }, + }); + } +} diff --git a/src/infrastructure/services/dexcomService/dexcomEntity.inteface.ts b/src/infrastructure/services/dexcomService/dexcomEntity.inteface.ts new file mode 100644 index 0000000..0f4f00e --- /dev/null +++ b/src/infrastructure/services/dexcomService/dexcomEntity.inteface.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +/** + Example of data + { + WT: 'Date(1674403723000)', + ST: 'Date(1674403723000)', + DT: 'Date(1674403723000+0100)', + Value: 115, + Trend: 'Flat' + } + */ + +const dateRegex = /^Date\(\d+\)$/; +const dateWithTimezoneRegex = /^Date\(\d+[+-]\d{4}\)$/; + +export const dexcomEntitySchema = z.object({ + WT: z.string().regex(dateRegex), + ST: z.string().regex(dateRegex), + DT: z.string().regex(dateWithTimezoneRegex), + Value: z.number().optional(), + Trend: z.string(), +}); + +export type IDexcomEntity = z.infer; diff --git a/src/infrastructure/services/dexcomService/dexcomService.ts b/src/infrastructure/services/dexcomService/dexcomService.ts new file mode 100644 index 0000000..8d987b5 --- /dev/null +++ b/src/infrastructure/services/dexcomService/dexcomService.ts @@ -0,0 +1,188 @@ +import { + GetReadingsFailure, + GetReadingsResult, + GetReadingsSuccess, + IDexcomService, +} from '../../../application/services/dexcomService/dexcomService.interface'; +import { IDexcomAuth } from './auth/dexcomAuth.interface'; +import { IDexcomRoute } from './routing/dexcomRoute.interface'; +import axios, { AxiosError } from 'axios'; +import { dexcomEntitySchema, IDexcomEntity } from './dexcomEntity.inteface'; +import { IDexcomEntityMapper } from './mappers/dexcomEntityMapper.interface'; + +export class DexcomService implements IDexcomService { + private readonly MAX_LOOP_ITERATION = 2; + + constructor( + private readonly dexcomAuth: IDexcomAuth, + private readonly dexcomRoute: IDexcomRoute, + private readonly dexcomEntityMapper: IDexcomEntityMapper, + private sessionId: string | null = null, + ) {} + + public async getReadings(data: { + minutesBefore: number; + maxCount: number; + }): Promise { + const { minutesBefore, maxCount } = data; + + if (minutesBefore < 1 || minutesBefore > 1440) { + new GetReadingsFailure({ + errorMessage: 'Minutes must be between 1 and 1440', + errorCode: 'ARG_ERROR_MINUTES_INVALID', + errorType: 'DOMAIN_ERROR', + context: { + minutesBefore, + }, + }); + } + + if (maxCount < 1 || maxCount > 287) { + new GetReadingsFailure({ + errorMessage: 'Max count must be between 1 and 287', + errorCode: 'ARG_ERROR_MINUTES_INVALID', + errorType: 'DOMAIN_ERROR', + context: { + minutesBefore, + }, + }); + } + + let i = 0; + let shouldBeRepeated = false; + let httpRequestResult: IDexcomEntity[] | GetReadingsFailure; + + do { + shouldBeRepeated = false; + + if (!this.sessionId) { + const authStateResult = await this.dexcomAuth.createAuthState(); + + if (authStateResult.isFailure()) { + return authStateResult; + } + + this.sessionId = authStateResult.getData().auth.sessionId; + } + + httpRequestResult = await this.getLatestReadingsHttpRequest({ + sessionId: this.sessionId, + maxCount, + minutesBefore, + }); + + if ( + httpRequestResult instanceof GetReadingsFailure && + httpRequestResult.getError().errorCode === 'SESSION_ID_EXPIRED' + ) { + shouldBeRepeated = true; + this.sessionId = null; + } + + i++; + } while (i < this.MAX_LOOP_ITERATION && shouldBeRepeated); + + if (httpRequestResult instanceof GetReadingsFailure) { + return httpRequestResult; + } + + const dexcomEntitySchemaResult = dexcomEntitySchema + .array() + .safeParse(httpRequestResult); + + if (!dexcomEntitySchemaResult.success) { + return new GetReadingsFailure({ + errorMessage: 'Schema not match', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'UNKNOWN_SCHEMA_ERROR', + context: { + data: httpRequestResult, + schemaError: dexcomEntitySchemaResult.error, + }, + }); + } + + const dexcomEntities: IDexcomEntity[] = dexcomEntitySchemaResult.data; + + return new GetReadingsSuccess({ + readings: dexcomEntities.map((dexcomEnity) => + this.dexcomEntityMapper.mapIntoDexcomReading(dexcomEnity), + ), + }); + } + + private async getLatestReadingsHttpRequest(data: { + sessionId: string; + minutesBefore: number; + maxCount: number; + }): Promise { + const { sessionId, maxCount, minutesBefore } = data; + + const query = { + sessionID: sessionId, + minutes: minutesBefore, + maxCount, + }; + + try { + const response = await axios.post( + this.dexcomRoute.getLatestGlucoseReadingsUrl(), + {}, + { params: query }, + ); + + return response.data; + } catch (error) { + if (!(error instanceof AxiosError)) { + return new GetReadingsFailure({ + errorMessage: 'Something went wrong during axios post request', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'UNKNOWN_LIBRARY_ERROR', + context: { + library: 'axios', + }, + }); + } + + if (error.response?.data.code === 'SessionNotValid') { + return new GetReadingsFailure({ + errorMessage: 'Session id is expired', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'SESSION_ID_EXPIRED', + context: { + data: error.response.data, + }, + }); + } + + if (error.response?.data.code === 'SessionIdNotFound') { + return new GetReadingsFailure({ + errorMessage: 'Session id is invalid', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'SESSION_ID_NOT_FOUND', + context: { + data: error.response.data, + }, + }); + } + + if (error.response?.data.code === 'InvalidArgument') { + return new GetReadingsFailure({ + errorMessage: 'Invalid auth argument', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'INVALID_AUTH_CREDENTIALS', + context: { + data: error.response.data, + }, + }); + } + + return new GetReadingsFailure({ + errorMessage: 'Something went wrong during request', + errorType: 'INFRASTRUCTURE_ERROR', + errorCode: 'HTTP_REQUEST_ERROR', + context: { ...error }, + }); + } + } +} diff --git a/src/infrastructure/services/dexcomService/envDexcomConfig.interface.ts b/src/infrastructure/services/dexcomService/envDexcomConfig.interface.ts new file mode 100644 index 0000000..b729052 --- /dev/null +++ b/src/infrastructure/services/dexcomService/envDexcomConfig.interface.ts @@ -0,0 +1,8 @@ +import { UserLocation } from '../../../common/types/userLocation'; + +export interface IEnvDexcomConfig { + getDexcomUsername: () => string; + getDexcomPassword: () => string; + getDexcomApplicationId: () => string; + getDexcomUserLocation: () => UserLocation; +} diff --git a/src/infrastructure/services/dexcomService/mappers/__tests__/dexcomEntityMapper.unit.test.ts b/src/infrastructure/services/dexcomService/mappers/__tests__/dexcomEntityMapper.unit.test.ts new file mode 100644 index 0000000..f008222 --- /dev/null +++ b/src/infrastructure/services/dexcomService/mappers/__tests__/dexcomEntityMapper.unit.test.ts @@ -0,0 +1,28 @@ +import { IDexcomEntityMapper } from '../dexcomEntityMapper.interface'; +import { DexcomEntityMapper } from '../dexcomEntityMapper'; +import { dexcomEntityTestFactory } from '../../__tests__/dexcomEntityTestFactory'; +import { faker } from '@faker-js/faker'; + +describe('DexcomEntityMapper', () => { + let dexcomEntityMapper: IDexcomEntityMapper; + + beforeEach(() => { + dexcomEntityMapper = new DexcomEntityMapper(); + }); + + describe('mapIntoDexcomReading', () => { + it('should correctly map the input into a DexcomReading object', () => { + const valueDate = faker.date.past(); + + const dexcomEntity = dexcomEntityTestFactory({ valueDate }); + + const result = dexcomEntityMapper.mapIntoDexcomReading(dexcomEntity); + + expect(result).toEqual({ + value: dexcomEntity.Value, + trend: dexcomEntity.Trend, + valueDate, + }); + }); + }); +}); diff --git a/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.interface.ts b/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.interface.ts new file mode 100644 index 0000000..61de551 --- /dev/null +++ b/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.interface.ts @@ -0,0 +1,6 @@ +import { IDexcomReading } from '../../../../application/services/dexcomService/dexcomReading.interface'; +import { IDexcomEntity } from '../dexcomEntity.inteface'; + +export interface IDexcomEntityMapper { + mapIntoDexcomReading: (dexcomEntity: IDexcomEntity) => IDexcomReading; +} diff --git a/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.ts b/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.ts new file mode 100644 index 0000000..4d93a7f --- /dev/null +++ b/src/infrastructure/services/dexcomService/mappers/dexcomEntityMapper.ts @@ -0,0 +1,19 @@ +import { IDexcomEntityMapper } from './dexcomEntityMapper.interface'; +import { IDexcomEntity } from '../dexcomEntity.inteface'; +import { IDexcomReading } from '../../../../application/services/dexcomService/dexcomReading.interface'; +import { CgmGlucoseTrend } from '../../../../domain/entities/cgmGlucose/cgmGlucose'; + +export class DexcomEntityMapper implements IDexcomEntityMapper { + mapIntoDexcomReading(dexcomEntity: IDexcomEntity): IDexcomReading { + const input = dexcomEntity.DT; + + const timestamp = (input.match(/\d+/) as RegExpMatchArray)[0]; + const date = new Date(parseInt(timestamp)); + + return { + value: dexcomEntity.Value, + trend: dexcomEntity.Trend as CgmGlucoseTrend, + valueDate: date, + }; + } +} diff --git a/src/infrastructure/services/dexcomService/routing/dexcomAuthRoute.inteface.ts b/src/infrastructure/services/dexcomService/routing/dexcomAuthRoute.inteface.ts new file mode 100644 index 0000000..1019543 --- /dev/null +++ b/src/infrastructure/services/dexcomService/routing/dexcomAuthRoute.inteface.ts @@ -0,0 +1,4 @@ +export interface IDexcomAuthRoute { + getAuthUrl: () => string; + getLoginUrl: () => string; +} diff --git a/src/infrastructure/services/dexcomService/routing/dexcomRoute.interface.ts b/src/infrastructure/services/dexcomService/routing/dexcomRoute.interface.ts new file mode 100644 index 0000000..af2771a --- /dev/null +++ b/src/infrastructure/services/dexcomService/routing/dexcomRoute.interface.ts @@ -0,0 +1,4 @@ +import { IDexcomAuthRoute } from './dexcomAuthRoute.inteface'; +import { IDexcomServiceRoute } from './dexcomServiceRoute.inteface'; + +export interface IDexcomRoute extends IDexcomAuthRoute, IDexcomServiceRoute {} diff --git a/src/infrastructure/services/dexcomService/routing/dexcomRoutes.ts b/src/infrastructure/services/dexcomService/routing/dexcomRoutes.ts new file mode 100644 index 0000000..b0ff792 --- /dev/null +++ b/src/infrastructure/services/dexcomService/routing/dexcomRoutes.ts @@ -0,0 +1,26 @@ +import { IEnvDexcomConfig } from '../envDexcomConfig.interface'; +import { UserLocation } from '../../../../common/types/userLocation'; +import { IDexcomRoute } from './dexcomRoute.interface'; + +export class DexcomRoute implements IDexcomRoute { + private readonly baseUrl: string; + + constructor(private readonly config: IEnvDexcomConfig) { + this.baseUrl = + this.config.getDexcomUserLocation() === UserLocation.US + ? 'https://share2.dexcom.com/ShareWebServices/Services' + : 'https://shareous1.dexcom.com/ShareWebServices/Services'; + } + + getAuthUrl(): string { + return `${this.baseUrl}/General/AuthenticatePublisherAccount`; + } + + getLatestGlucoseReadingsUrl(): string { + return `${this.baseUrl}/Publisher/ReadPublisherLatestGlucoseValues`; + } + + getLoginUrl(): string { + return `${this.baseUrl}/General/LoginPublisherAccountById`; + } +} diff --git a/src/infrastructure/services/dexcomService/routing/dexcomRoutes.unit.test.ts b/src/infrastructure/services/dexcomService/routing/dexcomRoutes.unit.test.ts new file mode 100644 index 0000000..88c28b5 --- /dev/null +++ b/src/infrastructure/services/dexcomService/routing/dexcomRoutes.unit.test.ts @@ -0,0 +1,80 @@ +import { IEnvDexcomConfig } from '../envDexcomConfig.interface'; +import { IDexcomRoute } from './dexcomRoute.interface'; +import { DexcomRoute } from './dexcomRoutes'; +import { UserLocation } from '../../../../common/types/userLocation'; + +describe('DexcomRoute', () => { + let config: IEnvDexcomConfig; + let route: IDexcomRoute; + + beforeEach(() => { + config = { + getDexcomUserLocation: jest.fn(), + getDexcomApplicationId: jest.fn(), + getDexcomPassword: jest.fn(), + getDexcomUsername: jest.fn(), + }; + }); + + describe('when location is US', () => { + beforeEach(() => { + jest.spyOn(config, 'getDexcomUserLocation').mockReturnValue(UserLocation.US); + + route = new DexcomRoute(config); + }); + describe('getAuthUrl', () => { + it('should return the correct Auth URL', () => { + expect(route.getAuthUrl()).toBe( + 'https://share2.dexcom.com/ShareWebServices/Services/General/AuthenticatePublisherAccount', + ); + }); + }); + + describe('getLatestGlucoseReadingsUrl', () => { + it('should return the correct Latest Glucose Readings URL', () => { + expect(route.getLatestGlucoseReadingsUrl()).toBe( + 'https://share2.dexcom.com/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues', + ); + }); + }); + + describe('getLoginUrl', () => { + it('should return the correct Login URL', () => { + expect(route.getLoginUrl()).toBe( + 'https://share2.dexcom.com/ShareWebServices/Services/General/LoginPublisherAccountById', + ); + }); + }); + }); + + describe('when location is EU', () => { + beforeEach(() => { + jest.spyOn(config, 'getDexcomUserLocation').mockReturnValue(UserLocation.EU); + route = new DexcomRoute(config); + }); + + describe('getAuthUrl', () => { + it('should return the correct Auth URL', () => { + expect(route.getAuthUrl()).toBe( + 'https://shareous1.dexcom.com/ShareWebServices/Services/General/AuthenticatePublisherAccount', + ); + }); + }); + + describe('getLatestGlucoseReadingsUrl', () => { + it('should return the correct Latest Glucose Readings URL', () => { + expect(route.getLatestGlucoseReadingsUrl()).toBe( + 'https://shareous1.dexcom.com/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues', + ); + }); + }); + + describe('getLoginUrl', () => { + it('should return the correct Login URL', () => { + expect(route.getLoginUrl()).toBe( + 'https://shareous1.dexcom.com/ShareWebServices/Services/General/LoginPublisherAccountById', + ); + }); + }); + }); +}); diff --git a/src/infrastructure/services/dexcomService/routing/dexcomServiceRoute.inteface.ts b/src/infrastructure/services/dexcomService/routing/dexcomServiceRoute.inteface.ts new file mode 100644 index 0000000..4ed4468 --- /dev/null +++ b/src/infrastructure/services/dexcomService/routing/dexcomServiceRoute.inteface.ts @@ -0,0 +1,3 @@ +export interface IDexcomServiceRoute { + getLatestGlucoseReadingsUrl: () => string; +}