Merge branch 'matcherModule' into 'main'
Matcher module See merge request v3/service/matcher!1
This commit is contained in:
commit
65b6042561
35
.env.dist
35
.env.dist
|
@ -4,6 +4,41 @@ SERVICE_PORT=5005
|
|||
SERVICE_CONFIGURATION_DOMAIN=MATCHER
|
||||
HEALTH_SERVICE_PORT=6005
|
||||
|
||||
# DEFAULT CONFIGURATION
|
||||
|
||||
# default identifier used for match requests
|
||||
DEFAULT_IDENTIFIER=0
|
||||
# default timezone
|
||||
DEFAULT_TIMEZONE=Europe/Paris
|
||||
# default number of seats proposed as driver
|
||||
DEFAULT_SEATS=3
|
||||
# algorithm type
|
||||
ALGORITHM=classic
|
||||
# strict algorithm (if relevant with the algorithm type)
|
||||
# if set to true, matches are made so that
|
||||
# punctual ads match only with punctual ads and
|
||||
# recurrent ads match only with recurrent ads
|
||||
STRICT_ALGORITHM=0
|
||||
# max distance in metres between driver
|
||||
# route and passenger pick-up / drop-off
|
||||
REMOTENESS=15000
|
||||
# use passenger proportion
|
||||
USE_PROPORTION=1
|
||||
# minimal driver proportion
|
||||
PROPORTION=0.3
|
||||
# use azimuth calculation
|
||||
USE_AZIMUTH=1
|
||||
# azimuth margin
|
||||
AZIMUTH_MARGIN=10
|
||||
# margin duration in seconds
|
||||
MARGIN_DURATION=900
|
||||
# default validity duration (in days) for recurrent proposals
|
||||
VALIDITY_DURATION=365
|
||||
# max detour ratio
|
||||
MAX_DETOUR_DISTANCE_RATIO=0.3
|
||||
MAX_DETOUR_DURATION_RATIO=0.3
|
||||
|
||||
|
||||
# PRISMA
|
||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ test:
|
|||
- docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d
|
||||
- sh ci/wait-up.sh
|
||||
- docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d
|
||||
- docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
|
||||
# - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
|
||||
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@grpc/proto-loader": "^0.7.6",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@nestjs/axios": "^2.0.0",
|
||||
"@nestjs/cache-manager": "^1.0.0",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
|
@ -24,9 +26,14 @@
|
|||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
"@prisma/client": "^4.12.0",
|
||||
"axios": "^1.3.5",
|
||||
"cache-manager": "^5.2.0",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"geo-tz": "^7.0.7",
|
||||
"geographiclib-geodesic": "^2.0.0",
|
||||
"got": "^11.8.6",
|
||||
"ioredis": "^5.3.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.2.0"
|
||||
|
@ -41,6 +48,7 @@
|
|||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@ -1569,6 +1577,28 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/axios": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-2.0.0.tgz",
|
||||
"integrity": "sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||
"axios": "^1.3.1",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cache-manager": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz",
|
||||
"integrity": "sha512-XMNdgsj3H+Ng/SYwFl13vRGNFA3e5Obk8LNwIuHLVSocnK2exReAWtscxEjQhoBc4FW4jAYOgU/U+mt18Q9T0g==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"cache-manager": "<=5",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz",
|
||||
|
@ -2102,6 +2132,17 @@
|
|||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinonjs/commons": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
|
@ -2120,6 +2161,17 @@
|
|||
"@sinonjs/commons": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
|
@ -2144,6 +2196,37 @@
|
|||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@turf/boolean-point-in-polygon": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
|
||||
"integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "^6.5.0",
|
||||
"@turf/invariant": "^6.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/helpers": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
|
||||
"integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/invariant": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
|
||||
"integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "^6.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz",
|
||||
|
@ -2195,6 +2278,17 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cacheable-request": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
"@types/node": "*",
|
||||
"@types/responselike": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
|
@ -2268,6 +2362,11 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
||||
|
@ -2308,6 +2407,14 @@
|
|||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
|
@ -2348,6 +2455,14 @@
|
|||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
|
||||
|
@ -2992,6 +3107,11 @@
|
|||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/array-source": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
|
||||
"integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw=="
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
|
@ -3010,8 +3130,17 @@
|
|||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
|
||||
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.5.0",
|
||||
|
@ -3401,6 +3530,18 @@
|
|||
"lru-cache": "~7.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager-ioredis-yet": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager-ioredis-yet/-/cache-manager-ioredis-yet-1.1.0.tgz",
|
||||
"integrity": "sha512-bGBAq8oNzzNkO2dwlYGWBxNXrz4w8FUTpe3nfUydJ6bm1ixKEcSUKYksGokQMaRgqkQjMbIHWFkvb8p+V9ZKqw==",
|
||||
"dependencies": {
|
||||
"cache-manager": "^5.1.0",
|
||||
"ioredis": "^5.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
|
@ -3409,6 +3550,45 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
|
||||
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^4.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^6.0.1",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request/node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
|
@ -3651,6 +3831,17 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
|
@ -3695,7 +3886,6 @@
|
|||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
|
@ -3856,6 +4046,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
|
@ -3889,11 +4104,18 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
@ -3992,6 +4214,21 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-cli": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
|
||||
"integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv-expand": "^10.0.0",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"dotenv": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
|
||||
|
@ -4040,7 +4277,6 @@
|
|||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
|
@ -4633,6 +4869,14 @@
|
|||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-source": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
|
||||
"integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
|
||||
"dependencies": {
|
||||
"stream-source": "0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
@ -4725,6 +4969,25 @@
|
|||
"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/fork-ts-checker-webpack-plugin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz",
|
||||
|
@ -4757,7 +5020,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
|
@ -4852,6 +5114,67 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/geo-tz": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-7.0.7.tgz",
|
||||
"integrity": "sha512-Aq0sRSO1y4w62D5muRqzDmN4SWfFYnt703BLiqiHAvunlwsJs4qd3Fkl1pKSUa0bwuBmPFxIA8M1E+ilg2PSjw==",
|
||||
"dependencies": {
|
||||
"@turf/boolean-point-in-polygon": "^6.5.0",
|
||||
"@turf/helpers": "^6.5.0",
|
||||
"geobuf": "^3.0.2",
|
||||
"pbf": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/geobuf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz",
|
||||
"integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==",
|
||||
"dependencies": {
|
||||
"concat-stream": "^2.0.0",
|
||||
"pbf": "^3.2.1",
|
||||
"shapefile": "~0.6.6"
|
||||
},
|
||||
"bin": {
|
||||
"geobuf2json": "bin/geobuf2json",
|
||||
"json2geobuf": "bin/json2geobuf",
|
||||
"shp2geobuf": "bin/shp2geobuf"
|
||||
}
|
||||
},
|
||||
"node_modules/geobuf/node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/geobuf/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/geographiclib-geodesic": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.0.0.tgz",
|
||||
"integrity": "sha512-qRE11UEF3Zn9VwDFf+Q1ZNn4VW2xwZWeAPiFRrKVSKn2K5lds1jOxhxgFJwbKh5YV58ME6+LGiRtm4A0CjFyiQ=="
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
|
@ -4967,6 +5290,30 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
"@types/cacheable-request": "^6.0.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
"cacheable-lookup": "^5.0.3",
|
||||
"cacheable-request": "^7.0.2",
|
||||
"decompress-response": "^6.0.0",
|
||||
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"responselike": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
|
@ -5024,6 +5371,11 @@
|
|||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
|
@ -5039,6 +5391,18 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
|
@ -5063,7 +5427,6 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
@ -6052,6 +6415,11 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
|
@ -6100,6 +6468,14 @@
|
|||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
|
||||
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
@ -6224,6 +6600,14 @@
|
|||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
@ -6396,6 +6780,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
@ -6562,6 +6954,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
|
@ -6605,7 +7008,6 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -6690,6 +7092,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
@ -6825,6 +7235,15 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/path-source": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
|
||||
"integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
|
||||
"dependencies": {
|
||||
"array-source": "0.0",
|
||||
"file-source": "0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
|
||||
|
@ -6839,6 +7258,18 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz",
|
||||
"integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==",
|
||||
"dependencies": {
|
||||
"ieee754": "^1.1.12",
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
|
@ -7069,6 +7500,11 @@
|
|||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
|
||||
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
@ -7081,11 +7517,15 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"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/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
|
@ -7155,6 +7595,17 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
@ -7298,6 +7749,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
|
||||
},
|
||||
"node_modules/resolve-cwd": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
|
||||
|
@ -7328,6 +7784,14 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve.exports": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
|
||||
|
@ -7337,6 +7801,17 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
|
@ -7635,6 +8110,28 @@
|
|||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/shapefile": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
|
||||
"integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
|
||||
"dependencies": {
|
||||
"array-source": "0.0",
|
||||
"commander": "2",
|
||||
"path-source": "0.1",
|
||||
"slice-source": "0.4",
|
||||
"stream-source": "0.3",
|
||||
"text-encoding": "^0.6.4"
|
||||
},
|
||||
"bin": {
|
||||
"dbf2json": "bin/dbf2json",
|
||||
"shp2json": "bin/shp2json"
|
||||
}
|
||||
},
|
||||
"node_modules/shapefile/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -7707,6 +8204,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-source": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
|
||||
"integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||
|
@ -7775,6 +8277,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-source": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
|
||||
"integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
|
@ -8051,6 +8558,12 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-encoding": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
||||
"integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==",
|
||||
"deprecated": "no longer maintained"
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
@ -8716,8 +9229,7 @@
|
|||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "4.0.2",
|
||||
|
|
30
package.json
30
package.json
|
@ -38,6 +38,8 @@
|
|||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@grpc/proto-loader": "^0.7.6",
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@nestjs/axios": "^2.0.0",
|
||||
"@nestjs/cache-manager": "^1.0.0",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^2.3.1",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
|
@ -46,9 +48,14 @@
|
|||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
"@prisma/client": "^4.12.0",
|
||||
"axios": "^1.3.5",
|
||||
"cache-manager": "^5.2.0",
|
||||
"cache-manager-ioredis-yet": "^1.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"geo-tz": "^7.0.7",
|
||||
"geographiclib-geodesic": "^2.0.0",
|
||||
"got": "^11.8.6",
|
||||
"ioredis": "^5.3.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.2.0"
|
||||
|
@ -63,6 +70,7 @@
|
|||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@ -83,6 +91,17 @@
|
|||
"json",
|
||||
"ts"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".enum.ts",
|
||||
"main.ts",
|
||||
"prisma-service.ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
|
@ -91,6 +110,17 @@
|
|||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".controller.ts",
|
||||
".module.ts",
|
||||
".request.ts",
|
||||
".presenter.ts",
|
||||
".profile.ts",
|
||||
".exception.ts",
|
||||
".enum.ts",
|
||||
"main.ts",
|
||||
"prisma-service.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS "postgis";
|
||||
|
||||
-- Required to use postgis extension :
|
||||
-- set the search_path to both public and territory (where is postgis) AND the current schema
|
||||
SET search_path TO matcher, territory, public;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ad" (
|
||||
"uuid" UUID NOT NULL,
|
||||
"driver" BOOLEAN NOT NULL,
|
||||
"passenger" BOOLEAN NOT NULL,
|
||||
"frequency" INTEGER NOT NULL,
|
||||
"from_date" DATE NOT NULL,
|
||||
"to_date" DATE NOT NULL,
|
||||
"mon_time" TIMESTAMPTZ NOT NULL,
|
||||
"tue_time" TIMESTAMPTZ NOT NULL,
|
||||
"wed_time" TIMESTAMPTZ NOT NULL,
|
||||
"thu_time" TIMESTAMPTZ NOT NULL,
|
||||
"fri_time" TIMESTAMPTZ NOT NULL,
|
||||
"sat_time" TIMESTAMPTZ NOT NULL,
|
||||
"sun_time" TIMESTAMPTZ NOT NULL,
|
||||
"mon_margin" INTEGER NOT NULL,
|
||||
"tue_margin" INTEGER NOT NULL,
|
||||
"wed_margin" INTEGER NOT NULL,
|
||||
"thu_margin" INTEGER NOT NULL,
|
||||
"fri_margin" INTEGER NOT NULL,
|
||||
"sat_margin" INTEGER NOT NULL,
|
||||
"sun_margin" INTEGER NOT NULL,
|
||||
"driver_duration" INTEGER NOT NULL,
|
||||
"driver_distance" INTEGER NOT NULL,
|
||||
"passenger_duration" INTEGER NOT NULL,
|
||||
"passenger_distance" INTEGER NOT NULL,
|
||||
"origin_type" SMALLINT NOT NULL,
|
||||
"destination_type" SMALLINT NOT NULL,
|
||||
"waypoints" geography(LINESTRING) NOT NULL,
|
||||
"direction" geography(LINESTRING) NOT NULL,
|
||||
"fwd_azimuth" INTEGER NOT NULL,
|
||||
"back_azimuth" INTEGER NOT NULL,
|
||||
"seats_driver" SMALLINT NOT NULL,
|
||||
"seats_passenger" SMALLINT NOT NULL,
|
||||
"seats_used" SMALLINT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ad_driver_idx" ON "ad"("driver");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ad_passenger_idx" ON "ad"("passenger");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ad_from_date_idx" ON "ad"("from_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ad_to_date_idx" ON "ad"("to_date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ad_fwd_azimuth_idx" ON "ad"("fwd_azimuth");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction");
|
|
@ -0,0 +1,3 @@
|
|||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
|
@ -3,12 +3,16 @@ import { AutomapperModule } from '@automapper/nestjs';
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigurationModule } from './modules/configuration/configuration.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
import { MatcherModule } from './modules/matcher/matcher.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
||||
ConfigurationModule,
|
||||
HealthModule,
|
||||
MatcherModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
|
|
@ -11,10 +11,9 @@ async function bootstrap() {
|
|||
app.connectMicroservice<MicroserviceOptions>({
|
||||
transport: Transport.GRPC,
|
||||
options: {
|
||||
// package: ['matcher', 'health'],
|
||||
package: ['health'],
|
||||
package: ['matcher', 'health'],
|
||||
protoPath: [
|
||||
// join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'),
|
||||
join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'),
|
||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
||||
],
|
||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
|
||||
import { getRedisToken } from '@liaoliaots/nestjs-redis';
|
||||
|
||||
const mockRedis = {
|
||||
get: jest.fn().mockResolvedValue('myValue'),
|
||||
set: jest.fn().mockImplementation(),
|
||||
del: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('RedisConfigurationRepository', () => {
|
||||
let redisConfigurationRepository: RedisConfigurationRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: getRedisToken('default'),
|
||||
useValue: mockRedis,
|
||||
},
|
||||
RedisConfigurationRepository,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
redisConfigurationRepository = module.get<RedisConfigurationRepository>(
|
||||
RedisConfigurationRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(redisConfigurationRepository).toBeDefined();
|
||||
});
|
||||
|
||||
describe('interact', () => {
|
||||
it('should get a value', async () => {
|
||||
expect(await redisConfigurationRepository.get('myKey')).toBe('myValue');
|
||||
});
|
||||
it('should set a value', async () => {
|
||||
expect(
|
||||
await redisConfigurationRepository.set('myKey', 'myValue'),
|
||||
).toBeUndefined();
|
||||
});
|
||||
it('should delete a value', async () => {
|
||||
expect(await redisConfigurationRepository.del('myKey')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { DatabaseException } from '../../exceptions/database.exception';
|
||||
import { ICollection } from '../../interfaces/collection.interface';
|
||||
import { IRepository } from '../../interfaces/repository.interface';
|
||||
|
@ -45,9 +45,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -66,8 +66,11 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(PrismaClientKnownRequestError.name, e.code);
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
);
|
||||
} else {
|
||||
throw new DatabaseException();
|
||||
}
|
||||
|
@ -85,9 +88,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -105,9 +108,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
});
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -131,9 +134,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return updatedEntity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -151,9 +154,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -171,9 +174,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
|
||||
return entity;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -204,9 +207,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
)}) VALUES (${Object.values(fields).join(',')})`;
|
||||
return await this._prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -225,9 +228,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
)} WHERE uuid = '${uuid}'`;
|
||||
return await this._prisma.$executeRawUnsafe(command);
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
@ -242,9 +245,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
|
|||
await this._prisma.$queryRaw`SELECT 1`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseException(
|
||||
PrismaClientKnownRequestError.name,
|
||||
Prisma.PrismaClientKnownRequestError.name,
|
||||
e.code,
|
||||
e.message,
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||
import { PrismaService } from '../../src/adapters/secondaries/prisma-service';
|
||||
import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract';
|
||||
import { DatabaseException } from '../../src/exceptions/database.exception';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
class FakeEntity {
|
||||
uuid?: string;
|
||||
|
@ -66,7 +66,7 @@ const mockPrismaService = {
|
|||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -78,7 +78,7 @@ const mockPrismaService = {
|
|||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((fields: object) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -90,7 +90,7 @@ const mockPrismaService = {
|
|||
$queryRaw: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -99,7 +99,7 @@ const mockPrismaService = {
|
|||
return true;
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new PrismaClientKnownRequestError('Database unavailable', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -110,7 +110,7 @@ const mockPrismaService = {
|
|||
.mockResolvedValueOnce(fakeEntityCreated)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -139,7 +139,7 @@ const mockPrismaService = {
|
|||
}
|
||||
|
||||
if (!entity && params?.where?.uuid == 'unknown') {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ const mockPrismaService = {
|
|||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -175,14 +175,14 @@ const mockPrismaService = {
|
|||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -212,7 +212,7 @@ const mockPrismaService = {
|
|||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
@ -236,7 +236,7 @@ const mockPrismaService = {
|
|||
.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.mockImplementationOnce((params?: any) => {
|
||||
throw new PrismaClientKnownRequestError('unknown request', {
|
||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { Controller } from '@nestjs/common';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0,
|
||||
SERVING = 1,
|
||||
NOT_SERVING = 2,
|
||||
}
|
||||
|
||||
interface HealthCheckRequest {
|
||||
service: string;
|
||||
}
|
||||
|
||||
interface HealthCheckResponse {
|
||||
status: ServingStatus;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class HealthServerController {
|
||||
constructor(
|
||||
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('Health', 'Check')
|
||||
async check(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
data: HealthCheckRequest,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
metadata: any,
|
||||
): Promise<HealthCheckResponse> {
|
||||
const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy(
|
||||
'prisma',
|
||||
);
|
||||
return {
|
||||
status:
|
||||
healthCheck['prisma'].status == 'up'
|
||||
? ServingStatus.SERVING
|
||||
: ServingStatus.NOT_SERVING,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckService,
|
||||
HealthCheck,
|
||||
HealthCheckResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { Messager } from '../secondaries/messager';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
|
||||
private _healthCheckService: HealthCheckService,
|
||||
private _messager: Messager,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
async check() {
|
||||
try {
|
||||
return await this._healthCheckService.check([
|
||||
async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
||||
]);
|
||||
} catch (error) {
|
||||
const healthCheckResult: HealthCheckResult = error.response;
|
||||
this._messager.publish(
|
||||
'logging.matcher.health.crit',
|
||||
JSON.stringify(healthCheckResult.error),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package health;
|
||||
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
|
||||
message HealthCheckRequest {
|
||||
string service = 1;
|
||||
}
|
||||
|
||||
message HealthCheckResponse {
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0;
|
||||
SERVING = 1;
|
||||
NOT_SERVING = 2;
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export abstract class IMessageBroker {
|
||||
exchange: string;
|
||||
|
||||
constructor(exchange: string) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
abstract publish(routingKey: string, message: string): void;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IMessageBroker } from './message-broker';
|
||||
|
||||
@Injectable()
|
||||
export class Messager extends IMessageBroker {
|
||||
constructor(
|
||||
private readonly _amqpConnection: AmqpConnection,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
||||
}
|
||||
|
||||
publish(routingKey: string, message: string): void {
|
||||
this._amqpConnection.publish(this.exchange, routingKey, message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
HealthCheckError,
|
||||
HealthIndicator,
|
||||
HealthIndicatorResult,
|
||||
} from '@nestjs/terminus';
|
||||
import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
|
||||
constructor(private readonly _repository: AdRepository) {
|
||||
super();
|
||||
}
|
||||
|
||||
async isHealthy(key: string): Promise<HealthIndicatorResult> {
|
||||
try {
|
||||
await this._repository.healthCheck();
|
||||
return this.getStatus(key, true);
|
||||
} catch (e) {
|
||||
throw new HealthCheckError('Prisma', {
|
||||
prisma: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { HealthServerController } from './adapters/primaries/health-server.controller';
|
||||
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { HealthController } from './adapters/primaries/health.controller';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { Messager } from './adapters/secondaries/messager';
|
||||
import { AdRepository } from '../matcher/adapters/secondaries/ad.repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TerminusModule,
|
||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
exchanges: [
|
||||
{
|
||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
||||
type: 'topic',
|
||||
},
|
||||
],
|
||||
uri: configService.get<string>('RMQ_URI'),
|
||||
connectionInitOptions: { wait: false },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
DatabaseModule,
|
||||
],
|
||||
controllers: [HealthServerController, HealthController],
|
||||
providers: [PrismaHealthIndicatorUseCase, AdRepository, Messager],
|
||||
})
|
||||
export class HealthModule {}
|
|
@ -0,0 +1,47 @@
|
|||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
|
||||
const mockAmqpConnection = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockResolvedValue({
|
||||
RMQ_EXCHANGE: 'mobicoop',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Messager', () => {
|
||||
let messager: Messager;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
Messager,
|
||||
{
|
||||
provide: AmqpConnection,
|
||||
useValue: mockAmqpConnection,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
messager = module.get<Messager>(Messager);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(messager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should publish a message', async () => {
|
||||
jest.spyOn(mockAmqpConnection, 'publish');
|
||||
messager.publish('test.create.info', 'my-test');
|
||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
|
||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
|
||||
import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository';
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||
|
||||
const mockAdRepository = {
|
||||
healthCheck: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
})
|
||||
.mockImplementation(() => {
|
||||
throw new PrismaClientKnownRequestError('Service unavailable', {
|
||||
code: 'code',
|
||||
clientVersion: 'version',
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
describe('PrismaHealthIndicatorUseCase', () => {
|
||||
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AdRepository,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
PrismaHealthIndicatorUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
|
||||
PrismaHealthIndicatorUseCase,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(prismaHealthIndicatorUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should check health successfully', async () => {
|
||||
const healthIndicatorResult: HealthIndicatorResult =
|
||||
await prismaHealthIndicatorUseCase.isHealthy('prisma');
|
||||
|
||||
expect(healthIndicatorResult['prisma'].status).toBe('up');
|
||||
});
|
||||
|
||||
it('should throw an error if database is unavailable', async () => {
|
||||
await expect(
|
||||
prismaHealthIndicatorUseCase.isHealthy('prisma'),
|
||||
).rejects.toBeInstanceOf(HealthCheckError);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from 'src/modules/utils/pipes/rpc.validation-pipe';
|
||||
import { MatchRequest } from '../../domain/dtos/match.request';
|
||||
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
|
||||
import { MatchQuery } from '../../queries/match.query';
|
||||
import { MatchPresenter } from '../secondaries/match.presenter';
|
||||
import { DefaultParamsProvider } from '../secondaries/default-params.provider';
|
||||
import { GeorouterCreator } from '../secondaries/georouter-creator';
|
||||
import { Match } from '../../domain/entities/ecosystem/match';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class MatcherController {
|
||||
constructor(
|
||||
private readonly _queryBus: QueryBus,
|
||||
private readonly _defaultParamsProvider: DefaultParamsProvider,
|
||||
@InjectMapper() private readonly _mapper: Mapper,
|
||||
private readonly _georouterCreator: GeorouterCreator,
|
||||
) {}
|
||||
|
||||
@GrpcMethod('MatcherService', 'Match')
|
||||
async match(data: MatchRequest): Promise<ICollection<Match>> {
|
||||
try {
|
||||
const matchCollection = await this._queryBus.execute(
|
||||
new MatchQuery(
|
||||
data,
|
||||
this._defaultParamsProvider.getParams(),
|
||||
this._georouterCreator,
|
||||
),
|
||||
);
|
||||
return Promise.resolve({
|
||||
data: matchCollection.data.map((match: Match) =>
|
||||
this._mapper.map(match, Match, MatchPresenter),
|
||||
),
|
||||
total: matchCollection.total,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package matcher;
|
||||
|
||||
service MatcherService {
|
||||
rpc Match(MatchRequest) returns (Matches);
|
||||
}
|
||||
|
||||
message MatchRequest {
|
||||
repeated Point waypoints = 1;
|
||||
string departure = 2;
|
||||
string fromDate = 3;
|
||||
Schedule schedule = 4;
|
||||
bool driver = 5;
|
||||
bool passenger = 6;
|
||||
string toDate = 7;
|
||||
int32 marginDuration = 8;
|
||||
MarginDurations marginDurations = 9;
|
||||
int32 seatsPassenger = 10;
|
||||
int32 seatsDriver = 11;
|
||||
bool strict = 12;
|
||||
Algorithm algorithm = 13;
|
||||
int32 remoteness = 14;
|
||||
bool useProportion = 15;
|
||||
int32 proportion = 16;
|
||||
bool useAzimuth = 17;
|
||||
int32 azimuthMargin = 18;
|
||||
float maxDetourDistanceRatio = 19;
|
||||
float maxDetourDurationRatio = 20;
|
||||
repeated int32 exclusions = 21;
|
||||
int32 identifier = 22;
|
||||
}
|
||||
|
||||
message Point {
|
||||
float lon = 1;
|
||||
float lat = 2;
|
||||
}
|
||||
|
||||
message Schedule {
|
||||
string mon = 1;
|
||||
string tue = 2;
|
||||
string wed = 3;
|
||||
string thu = 4;
|
||||
string fri = 5;
|
||||
string sat = 6;
|
||||
string sun = 7;
|
||||
}
|
||||
|
||||
message MarginDurations {
|
||||
int32 mon = 1;
|
||||
int32 tue = 2;
|
||||
int32 wed = 3;
|
||||
int32 thu = 4;
|
||||
int32 fri = 5;
|
||||
int32 sat = 6;
|
||||
int32 sun = 7;
|
||||
}
|
||||
|
||||
enum Algorithm {
|
||||
CLASSIC = 0;
|
||||
}
|
||||
|
||||
message Match {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
message Matches {
|
||||
repeated Match data = 1;
|
||||
int32 total = 2;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { MatcherRepository } from '../../../database/src/domain/matcher-repository';
|
||||
import { Ad } from '../../domain/entities/ecosystem/ad';
|
||||
|
||||
@Injectable()
|
||||
export class AdRepository extends MatcherRepository<Ad> {
|
||||
protected _model = 'ad';
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IDefaultParams } from '../../domain/types/default-params.type';
|
||||
|
||||
@Injectable()
|
||||
export class DefaultParamsProvider {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
getParams = (): IDefaultParams => {
|
||||
return {
|
||||
DEFAULT_IDENTIFIER: parseInt(
|
||||
this.configService.get('DEFAULT_IDENTIFIER'),
|
||||
),
|
||||
MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')),
|
||||
VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')),
|
||||
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
|
||||
DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
|
||||
DEFAULT_ALGORITHM_SETTINGS: {
|
||||
algorithm: this.configService.get('ALGORITHM'),
|
||||
strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
|
||||
remoteness: parseInt(this.configService.get('REMOTENESS')),
|
||||
useProportion: !!parseInt(this.configService.get('USE_PROPORTION')),
|
||||
proportion: parseInt(this.configService.get('PROPORTION')),
|
||||
useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')),
|
||||
azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')),
|
||||
maxDetourDistanceRatio: parseFloat(
|
||||
this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
|
||||
),
|
||||
maxDetourDurationRatio: parseFloat(
|
||||
this.configService.get('MAX_DETOUR_DURATION_RATIO'),
|
||||
),
|
||||
georouterType: this.configService.get('GEOROUTER_TYPE'),
|
||||
georouterUrl: this.configService.get('GEOROUTER_URL'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
|
||||
import { Geodesic, GeodesicClass } from 'geographiclib-geodesic';
|
||||
|
||||
@Injectable()
|
||||
export class MatcherGeodesic implements IGeodesic {
|
||||
_geod: GeodesicClass;
|
||||
|
||||
constructor() {
|
||||
this._geod = Geodesic.WGS84;
|
||||
}
|
||||
|
||||
inverse = (
|
||||
lon1: number,
|
||||
lat1: number,
|
||||
lon2: number,
|
||||
lat2: number,
|
||||
): { azimuth: number; distance: number } => {
|
||||
const { azi2: azimuth, s12: distance } = this._geod.Inverse(
|
||||
lat1,
|
||||
lon1,
|
||||
lat2,
|
||||
lon2,
|
||||
);
|
||||
return { azimuth, distance };
|
||||
};
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
|
||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
||||
import { GraphhopperGeorouter } from './graphhopper-georouter';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { MatcherGeodesic } from './geodesic';
|
||||
|
||||
@Injectable()
|
||||
export class GeorouterCreator implements ICreateGeorouter {
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly geodesic: MatcherGeodesic,
|
||||
) {}
|
||||
|
||||
create = (type: string, url: string): IGeorouter => {
|
||||
switch (type) {
|
||||
case 'graphhopper':
|
||||
return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
|
||||
default:
|
||||
throw new Error('Unknown geocoder');
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
import { HttpService } from '@nestjs/axios';
|
||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
||||
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
|
||||
import { Path } from '../../domain/types/path.type';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { catchError, lastValueFrom, map } from 'rxjs';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
|
||||
import { NamedRoute } from '../../domain/entities/ecosystem/named-route';
|
||||
import { Route } from '../../domain/entities/ecosystem/route';
|
||||
import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point';
|
||||
|
||||
@Injectable()
|
||||
export class GraphhopperGeorouter implements IGeorouter {
|
||||
_url: string;
|
||||
_urlArgs: Array<string>;
|
||||
_withTime: boolean;
|
||||
_withPoints: boolean;
|
||||
_withDistance: boolean;
|
||||
_paths: Array<Path>;
|
||||
_httpService: HttpService;
|
||||
_geodesic: IGeodesic;
|
||||
|
||||
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
|
||||
this._url = url + '/route?';
|
||||
this._httpService = httpService;
|
||||
this._geodesic = geodesic;
|
||||
}
|
||||
|
||||
route = async (
|
||||
paths: Array<Path>,
|
||||
settings: GeorouterSettings,
|
||||
): Promise<Array<NamedRoute>> => {
|
||||
this._setDefaultUrlArgs();
|
||||
this._setWithTime(settings.withTime);
|
||||
this._setWithPoints(settings.withPoints);
|
||||
this._setWithDistance(settings.withDistance);
|
||||
this._paths = paths;
|
||||
return await this._getRoutes();
|
||||
};
|
||||
|
||||
_setDefaultUrlArgs = (): void => {
|
||||
this._urlArgs = [
|
||||
'vehicle=car',
|
||||
'weighting=fastest',
|
||||
'points_encoded=false',
|
||||
];
|
||||
};
|
||||
|
||||
_setWithTime = (withTime: boolean): void => {
|
||||
this._withTime = withTime;
|
||||
if (withTime) {
|
||||
this._urlArgs.push('details=time');
|
||||
}
|
||||
};
|
||||
|
||||
_setWithPoints = (withPoints: boolean): void => {
|
||||
this._withPoints = withPoints;
|
||||
if (!withPoints) {
|
||||
this._urlArgs.push('calc_points=false');
|
||||
}
|
||||
};
|
||||
|
||||
_setWithDistance = (withDistance: boolean): void => {
|
||||
this._withDistance = withDistance;
|
||||
if (withDistance) {
|
||||
this._urlArgs.push('instructions=true');
|
||||
} else {
|
||||
this._urlArgs.push('instructions=false');
|
||||
}
|
||||
};
|
||||
|
||||
_getRoutes = async (): Promise<Array<NamedRoute>> => {
|
||||
const routes = Promise.all(
|
||||
this._paths.map(async (path) => {
|
||||
const url: string = [
|
||||
this._getUrl(),
|
||||
'&point=',
|
||||
path.points
|
||||
.map((point) => [point.lat, point.lon].join())
|
||||
.join('&point='),
|
||||
].join('');
|
||||
const route = await lastValueFrom(
|
||||
this._httpService.get(url).pipe(
|
||||
map((res) => (res.data ? this._createRoute(res) : undefined)),
|
||||
catchError((error: AxiosError) => {
|
||||
throw new Error('Georouter unavailable : ' + error.message);
|
||||
}),
|
||||
),
|
||||
);
|
||||
return <NamedRoute>{
|
||||
key: path.key,
|
||||
route,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return routes;
|
||||
};
|
||||
|
||||
_getUrl = (): string => {
|
||||
return [this._url, this._urlArgs.join('&')].join('');
|
||||
};
|
||||
|
||||
_createRoute = (response: AxiosResponse<GraphhopperResponse>): Route => {
|
||||
const route = new Route(this._geodesic);
|
||||
if (response.data.paths && response.data.paths[0]) {
|
||||
const shortestPath = response.data.paths[0];
|
||||
route.distance = shortestPath.distance ?? 0;
|
||||
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
|
||||
if (shortestPath.points && shortestPath.points.coordinates) {
|
||||
route.setPoints(
|
||||
shortestPath.points.coordinates.map((coordinate) => ({
|
||||
lon: coordinate[0],
|
||||
lat: coordinate[1],
|
||||
})),
|
||||
);
|
||||
if (
|
||||
shortestPath.details &&
|
||||
shortestPath.details.time &&
|
||||
shortestPath.snapped_waypoints &&
|
||||
shortestPath.snapped_waypoints.coordinates
|
||||
) {
|
||||
let instructions: Array<GraphhopperInstruction> = [];
|
||||
if (shortestPath.instructions)
|
||||
instructions = shortestPath.instructions;
|
||||
route.setSpacetimePoints(
|
||||
this._generateSpacetimePoints(
|
||||
shortestPath.points.coordinates,
|
||||
shortestPath.snapped_waypoints.coordinates,
|
||||
shortestPath.details.time,
|
||||
instructions,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
||||
_generateSpacetimePoints = (
|
||||
points: Array<Array<number>>,
|
||||
snappedWaypoints: Array<Array<number>>,
|
||||
durations: Array<Array<number>>,
|
||||
instructions: Array<GraphhopperInstruction>,
|
||||
): Array<SpacetimePoint> => {
|
||||
const indices = this._getIndices(points, snappedWaypoints);
|
||||
const times = this._getTimes(durations, indices);
|
||||
const distances = this._getDistances(instructions, indices);
|
||||
return indices.map(
|
||||
(index) =>
|
||||
new SpacetimePoint(
|
||||
points[index],
|
||||
times.find((time) => time.index == index)?.duration,
|
||||
distances.find((distance) => distance.index == index)?.distance,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
_getIndices = (
|
||||
points: Array<Array<number>>,
|
||||
snappedWaypoints: Array<Array<number>>,
|
||||
): Array<number> => {
|
||||
const indices = snappedWaypoints.map((waypoint) =>
|
||||
points.findIndex(
|
||||
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
|
||||
),
|
||||
);
|
||||
if (indices.find((index) => index == -1) === undefined) return indices;
|
||||
const missedWaypoints = indices
|
||||
.map(
|
||||
(value, index) =>
|
||||
<
|
||||
{
|
||||
index: number;
|
||||
originIndex: number;
|
||||
waypoint: Array<number>;
|
||||
nearest: number;
|
||||
distance: number;
|
||||
}
|
||||
>{
|
||||
index: value,
|
||||
originIndex: index,
|
||||
waypoint: snappedWaypoints[index],
|
||||
nearest: undefined,
|
||||
distance: 999999999,
|
||||
},
|
||||
)
|
||||
.filter((element) => element.index == -1);
|
||||
for (const index in points) {
|
||||
for (const missedWaypoint of missedWaypoints) {
|
||||
const inverse = this._geodesic.inverse(
|
||||
missedWaypoint.waypoint[0],
|
||||
missedWaypoint.waypoint[1],
|
||||
points[index][0],
|
||||
points[index][1],
|
||||
);
|
||||
if (inverse.distance < missedWaypoint.distance) {
|
||||
missedWaypoint.distance = inverse.distance;
|
||||
missedWaypoint.nearest = parseInt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const missedWaypoint of missedWaypoints) {
|
||||
indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
|
||||
}
|
||||
return indices;
|
||||
};
|
||||
|
||||
_getTimes = (
|
||||
durations: Array<Array<number>>,
|
||||
indices: Array<number>,
|
||||
): Array<{ index: number; duration: number }> => {
|
||||
const times: Array<{ index: number; duration: number }> = [];
|
||||
let duration = 0;
|
||||
for (const [origin, destination, stepDuration] of durations) {
|
||||
let indexFound = false;
|
||||
const indexAsOrigin = indices.find((index) => index == origin);
|
||||
if (
|
||||
indexAsOrigin !== undefined &&
|
||||
times.find((time) => origin == time.index) == undefined
|
||||
) {
|
||||
times.push({
|
||||
index: indexAsOrigin,
|
||||
duration: Math.round(stepDuration / 1000),
|
||||
});
|
||||
indexFound = true;
|
||||
}
|
||||
if (!indexFound) {
|
||||
const indexAsDestination = indices.find(
|
||||
(index) => index == destination,
|
||||
);
|
||||
if (
|
||||
indexAsDestination !== undefined &&
|
||||
times.find((time) => destination == time.index) == undefined
|
||||
) {
|
||||
times.push({
|
||||
index: indexAsDestination,
|
||||
duration: Math.round((duration + stepDuration) / 1000),
|
||||
});
|
||||
indexFound = true;
|
||||
}
|
||||
}
|
||||
if (!indexFound) {
|
||||
const indexInBetween = indices.find(
|
||||
(index) => origin < index && index < destination,
|
||||
);
|
||||
if (indexInBetween !== undefined) {
|
||||
times.push({
|
||||
index: indexInBetween,
|
||||
duration: Math.round((duration + stepDuration / 2) / 1000),
|
||||
});
|
||||
}
|
||||
}
|
||||
duration += stepDuration;
|
||||
}
|
||||
return times;
|
||||
};
|
||||
|
||||
_getDistances = (
|
||||
instructions: Array<GraphhopperInstruction>,
|
||||
indices: Array<number>,
|
||||
): Array<{ index: number; distance: number }> => {
|
||||
let distance = 0;
|
||||
const distances: Array<{ index: number; distance: number }> = [
|
||||
{
|
||||
index: 0,
|
||||
distance,
|
||||
},
|
||||
];
|
||||
for (const instruction of instructions) {
|
||||
distance += instruction.distance;
|
||||
if (
|
||||
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
|
||||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
|
||||
indices.find((index) => index == instruction.interval[0]) !== undefined
|
||||
) {
|
||||
distances.push({
|
||||
index: instruction.interval[0],
|
||||
distance: Math.round(distance),
|
||||
});
|
||||
}
|
||||
}
|
||||
return distances;
|
||||
};
|
||||
}
|
||||
|
||||
type GraphhopperResponse = {
|
||||
paths: [
|
||||
{
|
||||
distance: number;
|
||||
weight: number;
|
||||
time: number;
|
||||
points_encoded: boolean;
|
||||
bbox: Array<number>;
|
||||
points: GraphhopperCoordinates;
|
||||
snapped_waypoints: GraphhopperCoordinates;
|
||||
details: {
|
||||
time: Array<Array<number>>;
|
||||
};
|
||||
instructions: Array<GraphhopperInstruction>;
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
type GraphhopperCoordinates = {
|
||||
coordinates: Array<Array<number>>;
|
||||
};
|
||||
|
||||
type GraphhopperInstruction = {
|
||||
distance: number;
|
||||
heading: number;
|
||||
sign: GraphhopperSign;
|
||||
interval: Array<number>;
|
||||
text: string;
|
||||
};
|
||||
|
||||
enum GraphhopperSign {
|
||||
SIGN_START = 0,
|
||||
SIGN_FINISH = 4,
|
||||
SIGN_WAYPOINT = 5,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class MatchPresenter {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export abstract class MessageBroker {
|
||||
exchange: string;
|
||||
|
||||
constructor(exchange: string) {
|
||||
this.exchange = exchange;
|
||||
}
|
||||
|
||||
abstract publish(routingKey: string, message: string): void;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MessageBroker } from './message-broker';
|
||||
|
||||
@Injectable()
|
||||
export class Messager extends MessageBroker {
|
||||
constructor(
|
||||
private readonly _amqpConnection: AmqpConnection,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
||||
}
|
||||
|
||||
publish = (routingKey: string, message: string): void => {
|
||||
this._amqpConnection.publish(this.exchange, routingKey, message);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { AutoMap } from '@automapper/classes';
|
||||
import { Point } from '../types/point.type';
|
||||
import { Schedule } from '../types/schedule.type';
|
||||
import { MarginDurations } from '../types/margin-durations.type';
|
||||
import { Algorithm } from '../types/algorithm.enum';
|
||||
import { IRequestTime } from '../interfaces/time-request.interface';
|
||||
import { IRequestPerson } from '../interfaces/person-request.interface';
|
||||
import { IRequestGeography } from '../interfaces/geography-request.interface';
|
||||
import { IRequestRequirement } from '../interfaces/requirement-request.interface';
|
||||
import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface';
|
||||
|
||||
export class MatchRequest
|
||||
implements
|
||||
IRequestTime,
|
||||
IRequestPerson,
|
||||
IRequestGeography,
|
||||
IRequestRequirement,
|
||||
IRequestAlgorithmSettings
|
||||
{
|
||||
@IsArray()
|
||||
@AutoMap()
|
||||
waypoints: Array<Point>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@AutoMap()
|
||||
departure: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@AutoMap()
|
||||
fromDate: string;
|
||||
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
schedule: Schedule;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@AutoMap()
|
||||
driver: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@AutoMap()
|
||||
passenger: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@AutoMap()
|
||||
toDate: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@AutoMap()
|
||||
marginDuration: number;
|
||||
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
marginDurations: MarginDurations;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(10)
|
||||
@AutoMap()
|
||||
seatsPassenger: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(10)
|
||||
@AutoMap()
|
||||
seatsDriver: number;
|
||||
|
||||
@IsOptional()
|
||||
@AutoMap()
|
||||
strict: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Algorithm)
|
||||
@AutoMap()
|
||||
algorithm: Algorithm;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@AutoMap()
|
||||
remoteness: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@AutoMap()
|
||||
useProportion: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
@AutoMap()
|
||||
proportion: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@AutoMap()
|
||||
useAzimuth: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(359)
|
||||
@AutoMap()
|
||||
azimuthMargin: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
@AutoMap()
|
||||
maxDetourDistanceRatio: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
@AutoMap()
|
||||
maxDetourDurationRatio: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
exclusions: Array<number>;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@AutoMap()
|
||||
identifier: number;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Role } from '../../types/role.enum';
|
||||
import { Step } from '../../types/step.enum';
|
||||
import { Person } from './person';
|
||||
|
||||
export class Actor {
|
||||
person: Person;
|
||||
role: Role;
|
||||
step: Step;
|
||||
|
||||
constructor(person: Person, role: Role, step: Step) {
|
||||
this.person = person;
|
||||
this.role = role;
|
||||
this.step = step;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class Ad {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface';
|
||||
import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type';
|
||||
import { Algorithm } from '../../types/algorithm.enum';
|
||||
import { TimingFrequency } from '../../types/timing';
|
||||
import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface';
|
||||
import { IGeorouter } from '../../interfaces/georouter.interface';
|
||||
|
||||
export class AlgorithmSettings {
|
||||
_algorithmSettingsRequest: IRequestAlgorithmSettings;
|
||||
_strict: boolean;
|
||||
algorithm: Algorithm;
|
||||
restrict: TimingFrequency;
|
||||
remoteness: number;
|
||||
useProportion: boolean;
|
||||
proportion: number;
|
||||
useAzimuth: boolean;
|
||||
azimuthMargin: number;
|
||||
maxDetourDurationRatio: number;
|
||||
maxDetourDistanceRatio: number;
|
||||
georouter: IGeorouter;
|
||||
|
||||
constructor(
|
||||
algorithmSettingsRequest: IRequestAlgorithmSettings,
|
||||
defaultAlgorithmSettings: DefaultAlgorithmSettings,
|
||||
frequency: TimingFrequency,
|
||||
georouterCreator: ICreateGeorouter,
|
||||
) {
|
||||
this._algorithmSettingsRequest = algorithmSettingsRequest;
|
||||
this.algorithm =
|
||||
algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm;
|
||||
this._strict =
|
||||
algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict;
|
||||
this.remoteness = algorithmSettingsRequest.remoteness
|
||||
? Math.abs(algorithmSettingsRequest.remoteness)
|
||||
: defaultAlgorithmSettings.remoteness;
|
||||
this.useProportion =
|
||||
algorithmSettingsRequest.useProportion ??
|
||||
defaultAlgorithmSettings.useProportion;
|
||||
this.proportion = algorithmSettingsRequest.proportion
|
||||
? Math.abs(algorithmSettingsRequest.proportion)
|
||||
: defaultAlgorithmSettings.proportion;
|
||||
this.useAzimuth =
|
||||
algorithmSettingsRequest.useAzimuth ??
|
||||
defaultAlgorithmSettings.useAzimuth;
|
||||
this.azimuthMargin = algorithmSettingsRequest.azimuthMargin
|
||||
? Math.abs(algorithmSettingsRequest.azimuthMargin)
|
||||
: defaultAlgorithmSettings.azimuthMargin;
|
||||
this.maxDetourDistanceRatio =
|
||||
algorithmSettingsRequest.maxDetourDistanceRatio ??
|
||||
defaultAlgorithmSettings.maxDetourDistanceRatio;
|
||||
this.maxDetourDurationRatio =
|
||||
algorithmSettingsRequest.maxDetourDurationRatio ??
|
||||
defaultAlgorithmSettings.maxDetourDurationRatio;
|
||||
this.georouter = georouterCreator.create(
|
||||
defaultAlgorithmSettings.georouterType,
|
||||
defaultAlgorithmSettings.georouterUrl,
|
||||
);
|
||||
if (this._strict) {
|
||||
this.restrict = frequency;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import { MatcherException } from '../../../exceptions/matcher.exception';
|
||||
import { IRequestGeography } from '../../interfaces/geography-request.interface';
|
||||
import { PointType } from '../../types/geography.enum';
|
||||
import { Point } from '../../types/point.type';
|
||||
import { find } from 'geo-tz';
|
||||
import { Route } from './route';
|
||||
import { Role } from '../../types/role.enum';
|
||||
import { IGeorouter } from '../../interfaces/georouter.interface';
|
||||
import { Waypoint } from './waypoint';
|
||||
import { Actor } from './actor';
|
||||
import { Person } from './person';
|
||||
import { Step } from '../../types/step.enum';
|
||||
import { Path } from '../../types/path.type';
|
||||
|
||||
export class Geography {
|
||||
_geographyRequest: IRequestGeography;
|
||||
_person: Person;
|
||||
_points: Array<Point>;
|
||||
originType: PointType;
|
||||
destinationType: PointType;
|
||||
timezones: Array<string>;
|
||||
driverRoute: Route;
|
||||
passengerRoute: Route;
|
||||
|
||||
constructor(
|
||||
geographyRequest: IRequestGeography,
|
||||
defaultTimezone: string,
|
||||
person: Person,
|
||||
) {
|
||||
this._geographyRequest = geographyRequest;
|
||||
this._person = person;
|
||||
this._points = [];
|
||||
this.originType = undefined;
|
||||
this.destinationType = undefined;
|
||||
this.timezones = [defaultTimezone];
|
||||
}
|
||||
|
||||
init = (): void => {
|
||||
this._validateWaypoints();
|
||||
this._setTimezones();
|
||||
this._setPointTypes();
|
||||
};
|
||||
|
||||
createRoutes = async (
|
||||
roles: Array<Role>,
|
||||
georouter: IGeorouter,
|
||||
): Promise<void> => {
|
||||
let driverWaypoints: Array<Waypoint> = [];
|
||||
let passengerWaypoints: Array<Waypoint> = [];
|
||||
const paths: Array<Path> = [];
|
||||
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
|
||||
if (this._points.length == 2) {
|
||||
// 2 points => same route for driver and passenger
|
||||
const commonPath: Path = {
|
||||
key: RouteKey.COMMON,
|
||||
points: this._points,
|
||||
};
|
||||
driverWaypoints = this._createWaypoints(commonPath.points, Role.DRIVER);
|
||||
passengerWaypoints = this._createWaypoints(
|
||||
commonPath.points,
|
||||
Role.PASSENGER,
|
||||
);
|
||||
paths.push(commonPath);
|
||||
} else {
|
||||
const driverPath: Path = {
|
||||
key: RouteKey.DRIVER,
|
||||
points: this._points,
|
||||
};
|
||||
driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER);
|
||||
const passengerPath: Path = {
|
||||
key: RouteKey.PASSENGER,
|
||||
points: [this._points[0], this._points[this._points.length - 1]],
|
||||
};
|
||||
passengerWaypoints = this._createWaypoints(
|
||||
passengerPath.points,
|
||||
Role.PASSENGER,
|
||||
);
|
||||
paths.push(driverPath, passengerPath);
|
||||
}
|
||||
} else if (roles.includes(Role.DRIVER)) {
|
||||
const driverPath: Path = {
|
||||
key: RouteKey.DRIVER,
|
||||
points: this._points,
|
||||
};
|
||||
driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER);
|
||||
paths.push(driverPath);
|
||||
} else if (roles.includes(Role.PASSENGER)) {
|
||||
const passengerPath: Path = {
|
||||
key: RouteKey.PASSENGER,
|
||||
points: [this._points[0], this._points[this._points.length - 1]],
|
||||
};
|
||||
passengerWaypoints = this._createWaypoints(
|
||||
passengerPath.points,
|
||||
Role.PASSENGER,
|
||||
);
|
||||
paths.push(passengerPath);
|
||||
}
|
||||
const routes = await georouter.route(paths, {
|
||||
withDistance: false,
|
||||
withPoints: true,
|
||||
withTime: false,
|
||||
});
|
||||
if (routes.some((route) => route.key == RouteKey.COMMON)) {
|
||||
this.driverRoute = routes.find(
|
||||
(route) => route.key == RouteKey.COMMON,
|
||||
).route;
|
||||
this.passengerRoute = routes.find(
|
||||
(route) => route.key == RouteKey.COMMON,
|
||||
).route;
|
||||
this.driverRoute.setWaypoints(driverWaypoints);
|
||||
this.passengerRoute.setWaypoints(passengerWaypoints);
|
||||
} else {
|
||||
if (routes.some((route) => route.key == RouteKey.DRIVER)) {
|
||||
this.driverRoute = routes.find(
|
||||
(route) => route.key == RouteKey.DRIVER,
|
||||
).route;
|
||||
this.driverRoute.setWaypoints(driverWaypoints);
|
||||
}
|
||||
if (routes.some((route) => route.key == RouteKey.PASSENGER)) {
|
||||
this.passengerRoute = routes.find(
|
||||
(route) => route.key == RouteKey.PASSENGER,
|
||||
).route;
|
||||
this.passengerRoute.setWaypoints(passengerWaypoints);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_validateWaypoints = (): void => {
|
||||
if (this._geographyRequest.waypoints.length < 2) {
|
||||
throw new MatcherException(3, 'At least 2 waypoints are required');
|
||||
}
|
||||
this._geographyRequest.waypoints.map((point) => {
|
||||
if (!this._isValidPoint(point)) {
|
||||
throw new MatcherException(
|
||||
3,
|
||||
`Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`,
|
||||
);
|
||||
}
|
||||
this._points.push(point);
|
||||
});
|
||||
};
|
||||
|
||||
_setTimezones = (): void => {
|
||||
this.timezones = find(
|
||||
this._geographyRequest.waypoints[0].lat,
|
||||
this._geographyRequest.waypoints[0].lon,
|
||||
);
|
||||
};
|
||||
|
||||
_setPointTypes = (): void => {
|
||||
this.originType =
|
||||
this._geographyRequest.waypoints[0].type ?? PointType.OTHER;
|
||||
this.destinationType =
|
||||
this._geographyRequest.waypoints[
|
||||
this._geographyRequest.waypoints.length - 1
|
||||
].type ?? PointType.OTHER;
|
||||
};
|
||||
|
||||
_isValidPoint = (point: Point): boolean =>
|
||||
this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat);
|
||||
|
||||
_isValidLongitude = (longitude: number): boolean =>
|
||||
longitude >= -180 && longitude <= 180;
|
||||
|
||||
_isValidLatitude = (latitude: number): boolean =>
|
||||
latitude >= -90 && latitude <= 90;
|
||||
|
||||
_createWaypoints = (points: Array<Point>, role: Role): Array<Waypoint> => {
|
||||
return points.map((point, index) => {
|
||||
const waypoint = new Waypoint(point);
|
||||
if (index == 0) {
|
||||
waypoint.addActor(new Actor(this._person, role, Step.START));
|
||||
} else if (index == points.length - 1) {
|
||||
waypoint.addActor(new Actor(this._person, role, Step.FINISH));
|
||||
} else {
|
||||
waypoint.addActor(new Actor(this._person, role, Step.INTERMEDIATE));
|
||||
}
|
||||
return waypoint;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export enum RouteKey {
|
||||
COMMON = 'common',
|
||||
DRIVER = 'driver',
|
||||
PASSENGER = 'passenger',
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { AutoMap } from '@automapper/classes';
|
||||
|
||||
export class Match {
|
||||
@AutoMap()
|
||||
uuid: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { Route } from './route';
|
||||
|
||||
export type NamedRoute = {
|
||||
key: string;
|
||||
route: Route;
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { IRequestPerson } from '../../interfaces/person-request.interface';
|
||||
|
||||
export class Person {
|
||||
_personRequest: IRequestPerson;
|
||||
_defaultIdentifier: number;
|
||||
_defaultMarginDuration: number;
|
||||
identifier: number;
|
||||
marginDurations: Array<number>;
|
||||
|
||||
constructor(
|
||||
personRequest: IRequestPerson,
|
||||
defaultIdentifier: number,
|
||||
defaultMarginDuration: number,
|
||||
) {
|
||||
this._personRequest = personRequest;
|
||||
this._defaultIdentifier = defaultIdentifier;
|
||||
this._defaultMarginDuration = defaultMarginDuration;
|
||||
}
|
||||
|
||||
init = (): void => {
|
||||
this.setIdentifier(
|
||||
this._personRequest.identifier ?? this._defaultIdentifier,
|
||||
);
|
||||
this.setMarginDurations([
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
this._defaultMarginDuration,
|
||||
]);
|
||||
};
|
||||
|
||||
setIdentifier = (identifier: number): void => {
|
||||
this.identifier = identifier;
|
||||
};
|
||||
|
||||
setMarginDurations = (marginDurations: Array<number>): void => {
|
||||
this.marginDurations = marginDurations;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { IRequestRequirement } from '../../interfaces/requirement-request.interface';
|
||||
|
||||
export class Requirement {
|
||||
_requirementRequest: IRequestRequirement;
|
||||
seatsDriver: number;
|
||||
seatsPassenger: number;
|
||||
|
||||
constructor(requirementRequest: IRequestRequirement, defaultSeats: number) {
|
||||
this._requirementRequest = requirementRequest;
|
||||
this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats;
|
||||
this.seatsPassenger = requirementRequest.seatsPassenger ?? 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { IGeodesic } from '../../interfaces/geodesic.interface';
|
||||
import { Point } from '../../types/point.type';
|
||||
import { SpacetimePoint } from './spacetime-point';
|
||||
import { Waypoint } from './waypoint';
|
||||
|
||||
export class Route {
|
||||
distance: number;
|
||||
duration: number;
|
||||
fwdAzimuth: number;
|
||||
backAzimuth: number;
|
||||
distanceAzimuth: number;
|
||||
waypoints: Array<Waypoint>;
|
||||
points: Array<Point>;
|
||||
spacetimePoints: Array<SpacetimePoint>;
|
||||
_geodesic: IGeodesic;
|
||||
|
||||
constructor(geodesic: IGeodesic) {
|
||||
this.distance = undefined;
|
||||
this.duration = undefined;
|
||||
this.fwdAzimuth = undefined;
|
||||
this.backAzimuth = undefined;
|
||||
this.distanceAzimuth = undefined;
|
||||
this.waypoints = [];
|
||||
this.points = [];
|
||||
this.spacetimePoints = [];
|
||||
this._geodesic = geodesic;
|
||||
}
|
||||
|
||||
setWaypoints = (waypoints: Array<Waypoint>): void => {
|
||||
this.waypoints = waypoints;
|
||||
this._setAzimuth(waypoints.map((waypoint) => waypoint.point));
|
||||
};
|
||||
|
||||
setPoints = (points: Array<Point>): void => {
|
||||
this.points = points;
|
||||
this._setAzimuth(points);
|
||||
};
|
||||
|
||||
setSpacetimePoints = (spacetimePoints: Array<SpacetimePoint>): void => {
|
||||
this.spacetimePoints = spacetimePoints;
|
||||
};
|
||||
|
||||
_setAzimuth = (points: Array<Point>): void => {
|
||||
const inverse = this._geodesic.inverse(
|
||||
points[0].lon,
|
||||
points[0].lat,
|
||||
points[points.length - 1].lon,
|
||||
points[points.length - 1].lat,
|
||||
);
|
||||
this.fwdAzimuth =
|
||||
inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth);
|
||||
this.backAzimuth =
|
||||
this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180;
|
||||
this.distanceAzimuth = inverse.distance;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export class SpacetimePoint {
|
||||
point: Array<number>;
|
||||
duration: number;
|
||||
distance: number;
|
||||
|
||||
constructor(point: Array<number>, duration: number, distance: number) {
|
||||
this.point = point;
|
||||
this.duration = duration;
|
||||
this.distance = distance;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
import { MatcherException } from '../../../exceptions/matcher.exception';
|
||||
import { MarginDurations } from '../../types/margin-durations.type';
|
||||
import { IRequestTime } from '../../interfaces/time-request.interface';
|
||||
import { TimingDays, TimingFrequency, Days } from '../../types/timing';
|
||||
import { Schedule } from '../../types/schedule.type';
|
||||
|
||||
export class Time {
|
||||
_timeRequest: IRequestTime;
|
||||
_defaultMarginDuration: number;
|
||||
_defaultValidityDuration: number;
|
||||
frequency: TimingFrequency;
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
schedule: Schedule;
|
||||
marginDurations: MarginDurations;
|
||||
|
||||
constructor(
|
||||
timeRequest: IRequestTime,
|
||||
defaultMarginDuration: number,
|
||||
defaultValidityDuration: number,
|
||||
) {
|
||||
this._timeRequest = timeRequest;
|
||||
this._defaultMarginDuration = defaultMarginDuration;
|
||||
this._defaultValidityDuration = defaultValidityDuration;
|
||||
this.schedule = {};
|
||||
this.marginDurations = {
|
||||
mon: defaultMarginDuration,
|
||||
tue: defaultMarginDuration,
|
||||
wed: defaultMarginDuration,
|
||||
thu: defaultMarginDuration,
|
||||
fri: defaultMarginDuration,
|
||||
sat: defaultMarginDuration,
|
||||
sun: defaultMarginDuration,
|
||||
};
|
||||
}
|
||||
|
||||
init = (): void => {
|
||||
this._validateBaseDate();
|
||||
this._validatePunctualRequest();
|
||||
this._validateRecurrentRequest();
|
||||
this._setPunctualRequest();
|
||||
this._setRecurrentRequest();
|
||||
this._setMargindurations();
|
||||
};
|
||||
|
||||
_validateBaseDate = (): void => {
|
||||
if (!this._timeRequest.departure && !this._timeRequest.fromDate) {
|
||||
throw new MatcherException(3, 'departure or fromDate is required');
|
||||
}
|
||||
};
|
||||
|
||||
_validatePunctualRequest = (): void => {
|
||||
if (this._timeRequest.departure) {
|
||||
this.fromDate = this.toDate = new Date(this._timeRequest.departure);
|
||||
if (!this._isDate(this.fromDate)) {
|
||||
throw new MatcherException(3, 'Wrong departure date');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_validateRecurrentRequest = (): void => {
|
||||
if (this._timeRequest.fromDate) {
|
||||
this.fromDate = new Date(this._timeRequest.fromDate);
|
||||
if (!this._isDate(this.fromDate)) {
|
||||
throw new MatcherException(3, 'Wrong fromDate');
|
||||
}
|
||||
}
|
||||
if (this._timeRequest.toDate) {
|
||||
this.toDate = new Date(this._timeRequest.toDate);
|
||||
if (!this._isDate(this.toDate)) {
|
||||
throw new MatcherException(3, 'Wrong toDate');
|
||||
}
|
||||
if (this.toDate < this.fromDate) {
|
||||
throw new MatcherException(3, 'toDate must be after fromDate');
|
||||
}
|
||||
}
|
||||
if (this._timeRequest.fromDate) {
|
||||
this._validateSchedule();
|
||||
}
|
||||
};
|
||||
|
||||
_validateSchedule = (): void => {
|
||||
if (!this._timeRequest.schedule) {
|
||||
throw new MatcherException(3, 'Schedule is required');
|
||||
}
|
||||
if (
|
||||
!Object.keys(this._timeRequest.schedule).some((elem) =>
|
||||
Days.includes(elem),
|
||||
)
|
||||
) {
|
||||
throw new MatcherException(3, 'No valid day in the given schedule');
|
||||
}
|
||||
Object.keys(this._timeRequest.schedule).map((day) => {
|
||||
const time = new Date('1970-01-01 ' + this._timeRequest.schedule[day]);
|
||||
if (!this._isDate(time)) {
|
||||
throw new MatcherException(3, `Wrong time for ${day} in schedule`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_setPunctualRequest = (): void => {
|
||||
if (this._timeRequest.departure) {
|
||||
this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL;
|
||||
this.schedule[TimingDays[this.fromDate.getDay()]] =
|
||||
this.fromDate.getHours() + ':' + this.fromDate.getMinutes();
|
||||
}
|
||||
};
|
||||
|
||||
_setRecurrentRequest = (): void => {
|
||||
if (this._timeRequest.fromDate) {
|
||||
this.frequency = TimingFrequency.FREQUENCY_RECURRENT;
|
||||
if (!this.toDate) {
|
||||
this.toDate = this._addDays(
|
||||
this.fromDate,
|
||||
this._defaultValidityDuration,
|
||||
);
|
||||
}
|
||||
this._setSchedule();
|
||||
}
|
||||
};
|
||||
|
||||
_setSchedule = (): void => {
|
||||
Object.keys(this._timeRequest.schedule).map((day) => {
|
||||
this.schedule[day] = this._timeRequest.schedule[day];
|
||||
});
|
||||
};
|
||||
|
||||
_setMargindurations = (): void => {
|
||||
if (this._timeRequest.marginDuration) {
|
||||
const duration = Math.abs(this._timeRequest.marginDuration);
|
||||
this.marginDurations = {
|
||||
mon: duration,
|
||||
tue: duration,
|
||||
wed: duration,
|
||||
thu: duration,
|
||||
fri: duration,
|
||||
sat: duration,
|
||||
sun: duration,
|
||||
};
|
||||
}
|
||||
if (this._timeRequest.marginDurations) {
|
||||
if (
|
||||
!Object.keys(this._timeRequest.marginDurations).some((elem) =>
|
||||
Days.includes(elem),
|
||||
)
|
||||
) {
|
||||
throw new MatcherException(
|
||||
3,
|
||||
'No valid day in the given margin durations',
|
||||
);
|
||||
}
|
||||
Object.keys(this._timeRequest.marginDurations).map((day) => {
|
||||
this.marginDurations[day] = Math.abs(
|
||||
this._timeRequest.marginDurations[day],
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_isDate = (date: Date): boolean => {
|
||||
return date instanceof Date && isFinite(+date);
|
||||
};
|
||||
|
||||
_addDays = (date: Date, days: number): Date => {
|
||||
const result = new Date(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Point } from '../../types/point.type';
|
||||
import { Actor } from './actor';
|
||||
|
||||
export class Waypoint {
|
||||
point: Point;
|
||||
actors: Array<Actor>;
|
||||
|
||||
constructor(point: Point) {
|
||||
this.point = point;
|
||||
this.actors = [];
|
||||
}
|
||||
|
||||
addActor = (actor: Actor) => this.actors.push(actor);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { Person } from '../ecosystem/person';
|
||||
|
||||
export class Candidate {
|
||||
person: Person;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
|
||||
import { Processor } from '../processor.abstract';
|
||||
import { Candidate } from '../candidate';
|
||||
|
||||
export abstract class AlgorithmFactory {
|
||||
_matchQuery: MatchQuery;
|
||||
_candidates: Array<Candidate>;
|
||||
|
||||
constructor(matchQuery: MatchQuery) {
|
||||
this._matchQuery = matchQuery;
|
||||
this._candidates = [];
|
||||
}
|
||||
|
||||
abstract createProcessors(): Array<Processor>;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { AlgorithmFactory } from './algorithm-factory.abstract';
|
||||
import { Processor } from '../processor.abstract';
|
||||
import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor';
|
||||
|
||||
export class ClassicAlgorithmFactory extends AlgorithmFactory {
|
||||
createProcessors(): Array<Processor> {
|
||||
return [new ClassicWaypointsCompleter(this._matchQuery)];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { MatchQuery } from '../../../queries/match.query';
|
||||
import { Algorithm } from '../../types/algorithm.enum';
|
||||
import { Match } from '../ecosystem/match';
|
||||
import { Candidate } from './candidate';
|
||||
import { AlgorithmFactory } from './factory/algorithm-factory.abstract';
|
||||
import { ClassicAlgorithmFactory } from './factory/classic';
|
||||
|
||||
export class Matcher {
|
||||
match = (matchQuery: MatchQuery): Array<Match> => {
|
||||
let algorithm: AlgorithmFactory;
|
||||
switch (matchQuery.algorithmSettings.algorithm) {
|
||||
case Algorithm.CLASSIC:
|
||||
algorithm = new ClassicAlgorithmFactory(matchQuery);
|
||||
}
|
||||
let candidates: Array<Candidate> = [];
|
||||
for (const processor of algorithm.createProcessors()) {
|
||||
candidates = processor.execute(candidates);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
|
||||
import { Candidate } from './candidate';
|
||||
|
||||
export abstract class Processor {
|
||||
_matchQuery: MatchQuery;
|
||||
|
||||
constructor(matchQuery: MatchQuery) {
|
||||
this._matchQuery = matchQuery;
|
||||
}
|
||||
|
||||
abstract execute(candidates: Array<Candidate>): Array<Candidate>;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Candidate } from '../../candidate';
|
||||
import { Completer } from './completer.abstract';
|
||||
|
||||
export class ClassicWaypointsCompleter extends Completer {
|
||||
complete(candidates: Array<Candidate>): Array<Candidate> {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Candidate } from '../../candidate';
|
||||
import { Processor } from '../../processor.abstract';
|
||||
|
||||
export abstract class Completer extends Processor {
|
||||
execute = (candidates: Array<Candidate>): Array<Candidate> =>
|
||||
this.complete(candidates);
|
||||
|
||||
abstract complete(candidates: Array<Candidate>): Array<Candidate>;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { Algorithm } from '../types/algorithm.enum';
|
||||
|
||||
export interface IRequestAlgorithmSettings {
|
||||
algorithm: Algorithm;
|
||||
strict: boolean;
|
||||
remoteness: number;
|
||||
useProportion: boolean;
|
||||
proportion: number;
|
||||
useAzimuth: boolean;
|
||||
azimuthMargin: number;
|
||||
maxDetourDistanceRatio: number;
|
||||
maxDetourDurationRatio: number;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export interface IGeodesic {
|
||||
inverse(
|
||||
lon1: number,
|
||||
lat1: number,
|
||||
lon2: number,
|
||||
lat2: number,
|
||||
): {
|
||||
azimuth: number;
|
||||
distance: number;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { Point } from '../types/point.type';
|
||||
|
||||
export interface IRequestGeography {
|
||||
waypoints: Array<Point>;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IGeorouter } from './georouter.interface';
|
||||
|
||||
export interface ICreateGeorouter {
|
||||
create(type: string, url: string): IGeorouter;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { NamedRoute } from '../entities/ecosystem/named-route';
|
||||
import { GeorouterSettings } from '../types/georouter-settings.type';
|
||||
import { Path } from '../types/path.type';
|
||||
|
||||
export interface IGeorouter {
|
||||
route(
|
||||
paths: Array<Path>,
|
||||
settings: GeorouterSettings,
|
||||
): Promise<Array<NamedRoute>>;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IRequestPerson {
|
||||
identifier?: number;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface IRequestRequirement {
|
||||
seatsDriver?: number;
|
||||
seatsPassenger?: number;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { MarginDurations } from '../types/margin-durations.type';
|
||||
import { Schedule } from '../types/schedule.type';
|
||||
|
||||
export interface IRequestTime {
|
||||
departure?: string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
schedule?: Schedule;
|
||||
marginDuration?: number;
|
||||
marginDurations?: MarginDurations;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { Person } from '../entities/ecosystem/person';
|
||||
import { Role } from './role.enum';
|
||||
import { Step } from './step.enum';
|
||||
|
||||
export type Actor = {
|
||||
person: Person;
|
||||
role: Role;
|
||||
step: Step;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export enum Algorithm {
|
||||
CLASSIC = 'CLASSIC',
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Algorithm } from './algorithm.enum';
|
||||
|
||||
export type DefaultAlgorithmSettings = {
|
||||
algorithm: Algorithm;
|
||||
strict: boolean;
|
||||
remoteness: number;
|
||||
useProportion: boolean;
|
||||
proportion: number;
|
||||
useAzimuth: boolean;
|
||||
azimuthMargin: number;
|
||||
maxDetourDistanceRatio: number;
|
||||
maxDetourDurationRatio: number;
|
||||
georouterType: string;
|
||||
georouterUrl: string;
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { DefaultAlgorithmSettings } from './default-algorithm-settings.type';
|
||||
|
||||
export type IDefaultParams = {
|
||||
DEFAULT_IDENTIFIER: number;
|
||||
MARGIN_DURATION: number;
|
||||
VALIDITY_DURATION: number;
|
||||
DEFAULT_TIMEZONE: string;
|
||||
DEFAULT_SEATS: number;
|
||||
DEFAULT_ALGORITHM_SETTINGS: DefaultAlgorithmSettings;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export enum PointType {
|
||||
HOUSE_NUMBER = 'HOUSE_NUMBER',
|
||||
STREET_ADDRESS = 'STREET_ADDRESS',
|
||||
LOCALITY = 'LOCALITY',
|
||||
VENUE = 'VENUE',
|
||||
OTHER = 'OTHER',
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type GeorouterSettings = {
|
||||
withPoints: boolean;
|
||||
withTime: boolean;
|
||||
withDistance: boolean;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
export type MarginDurations = {
|
||||
mon?: number;
|
||||
tue?: number;
|
||||
wed?: number;
|
||||
thu?: number;
|
||||
fri?: number;
|
||||
sat?: number;
|
||||
sun?: number;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { Point } from './point.type';
|
||||
|
||||
export type Path = {
|
||||
key: string;
|
||||
points: Array<Point>;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { PointType } from './geography.enum';
|
||||
|
||||
export type Point = {
|
||||
lon: number;
|
||||
lat: number;
|
||||
type?: PointType;
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export enum Role {
|
||||
DRIVER = 'DRIVER',
|
||||
PASSENGER = 'PASSENGER',
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export type Schedule = {
|
||||
mon?: string;
|
||||
tue?: string;
|
||||
wed?: string;
|
||||
thu?: string;
|
||||
fri?: string;
|
||||
sat?: string;
|
||||
sun?: string;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export enum Step {
|
||||
START = 'start',
|
||||
INTERMEDIATE = 'intermediate',
|
||||
NEUTRAL = 'neutral',
|
||||
FINISH = 'finish',
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export enum TimingFrequency {
|
||||
FREQUENCY_PUNCTUAL = 1,
|
||||
FREQUENCY_RECURRENT = 2,
|
||||
}
|
||||
|
||||
export enum TimingDays {
|
||||
'sun',
|
||||
'mon',
|
||||
'tue',
|
||||
'wed',
|
||||
'thu',
|
||||
'fri',
|
||||
'sat',
|
||||
}
|
||||
|
||||
export const Days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
@ -0,0 +1,7 @@
|
|||
import { Actor } from './actor.type.';
|
||||
import { Point } from './point.type';
|
||||
|
||||
export type Waypoint = {
|
||||
point: Point;
|
||||
actors: Array<Actor>;
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
import { Mapper } from '@automapper/core';
|
||||
import { InjectMapper } from '@automapper/nestjs';
|
||||
import { QueryHandler } from '@nestjs/cqrs';
|
||||
import { Messager } from '../../adapters/secondaries/messager';
|
||||
import { MatchQuery } from '../../queries/match.query';
|
||||
import { AdRepository } from '../../adapters/secondaries/ad.repository';
|
||||
import { Match } from '../entities/ecosystem/match';
|
||||
import { ICollection } from '../../../database/src/interfaces/collection.interface';
|
||||
|
||||
@QueryHandler(MatchQuery)
|
||||
export class MatchUseCase {
|
||||
constructor(
|
||||
private readonly _repository: AdRepository,
|
||||
private readonly _messager: Messager,
|
||||
@InjectMapper() private readonly _mapper: Mapper,
|
||||
) {}
|
||||
|
||||
execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => {
|
||||
try {
|
||||
// const paths = [];
|
||||
// for (let i = 0; i < 1; i++) {
|
||||
// paths.push({
|
||||
// key: 'route' + i,
|
||||
// points: [
|
||||
// {
|
||||
// lat: 48.110899,
|
||||
// lon: -1.68365,
|
||||
// },
|
||||
// {
|
||||
// lat: 48.131105,
|
||||
// lon: -1.690067,
|
||||
// },
|
||||
// {
|
||||
// lat: 48.534769,
|
||||
// lon: -1.894032,
|
||||
// },
|
||||
// {
|
||||
// lat: 48.56516,
|
||||
// lon: -1.923553,
|
||||
// },
|
||||
// {
|
||||
// lat: 48.622813,
|
||||
// lon: -1.997177,
|
||||
// },
|
||||
// {
|
||||
// lat: 48.67846,
|
||||
// lon: -1.8554,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
// const routes = await matchQuery.algorithmSettings.georouter.route(paths, {
|
||||
// withDistance: false,
|
||||
// withPoints: true,
|
||||
// withTime: true,
|
||||
// });
|
||||
// routes.map((route) => console.log(route.route.spacetimePoints));
|
||||
const match = new Match();
|
||||
match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85';
|
||||
this._messager.publish('matcher.match', 'match !');
|
||||
return {
|
||||
data: [match],
|
||||
total: 1,
|
||||
};
|
||||
} catch (error) {
|
||||
const err: Error = error;
|
||||
this._messager.publish(
|
||||
'logging.matcher.match.crit',
|
||||
JSON.stringify({
|
||||
matchQuery,
|
||||
error: err.message,
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export class MatcherException implements Error {
|
||||
name: string;
|
||||
message: string;
|
||||
|
||||
constructor(private _code: number, private _message: string) {
|
||||
this.name = 'MatcherException';
|
||||
this.message = _message;
|
||||
}
|
||||
|
||||
get code(): number {
|
||||
return this._code;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { createMap, Mapper } from '@automapper/core';
|
||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MatchPresenter } from '../adapters/secondaries/match.presenter';
|
||||
import { Match } from '../domain/entities/ecosystem/match';
|
||||
|
||||
@Injectable()
|
||||
export class MatchProfile extends AutomapperProfile {
|
||||
constructor(@InjectMapper() mapper: Mapper) {
|
||||
super(mapper);
|
||||
}
|
||||
|
||||
override get profile() {
|
||||
return (mapper: Mapper) => {
|
||||
createMap(mapper, Match, MatchPresenter);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { MatcherController } from './adapters/primaries/matcher.controller';
|
||||
import { MatchProfile } from './mappers/match.profile';
|
||||
import { AdRepository } from './adapters/secondaries/ad.repository';
|
||||
import { MatchUseCase } from './domain/usecases/match.usecase';
|
||||
import { Messager } from './adapters/secondaries/messager';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { RedisClientOptions } from '@liaoliaots/nestjs-redis';
|
||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
|
||||
import { GeorouterCreator } from './adapters/secondaries/georouter-creator';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { MatcherGeodesic } from './adapters/secondaries/geodesic';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
CqrsModule,
|
||||
HttpModule,
|
||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
exchanges: [
|
||||
{
|
||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
||||
type: 'topic',
|
||||
},
|
||||
],
|
||||
uri: configService.get<string>('RMQ_URI'),
|
||||
connectionInitOptions: { wait: false },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
CacheModule.registerAsync<RedisClientOptions>({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
store: await redisStore({
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT'),
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
ttl: configService.get<number>('CACHE_TTL'),
|
||||
}),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [MatcherController],
|
||||
providers: [
|
||||
MatchProfile,
|
||||
AdRepository,
|
||||
Messager,
|
||||
DefaultParamsProvider,
|
||||
MatchUseCase,
|
||||
GeorouterCreator,
|
||||
MatcherGeodesic,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class MatcherModule {}
|
|
@ -0,0 +1,103 @@
|
|||
import { MatchRequest } from '../domain/dtos/match.request';
|
||||
import { Geography } from '../domain/entities/ecosystem/geography';
|
||||
import { Person } from '../domain/entities/ecosystem/person';
|
||||
import { Requirement } from '../domain/entities/ecosystem/requirement';
|
||||
import { Role } from '../domain/types/role.enum';
|
||||
import { AlgorithmSettings } from '../domain/entities/ecosystem/algorithm-settings';
|
||||
import { Time } from '../domain/entities/ecosystem/time';
|
||||
import { IDefaultParams } from '../domain/types/default-params.type';
|
||||
import { IGeorouter } from '../domain/interfaces/georouter.interface';
|
||||
import { ICreateGeorouter } from '../domain/interfaces/georouter-creator.interface';
|
||||
|
||||
export class MatchQuery {
|
||||
private readonly _matchRequest: MatchRequest;
|
||||
private readonly _defaultParams: IDefaultParams;
|
||||
private readonly _georouterCreator: ICreateGeorouter;
|
||||
person: Person;
|
||||
roles: Array<Role>;
|
||||
time: Time;
|
||||
geography: Geography;
|
||||
exclusions: Array<number>;
|
||||
requirement: Requirement;
|
||||
algorithmSettings: AlgorithmSettings;
|
||||
georouter: IGeorouter;
|
||||
|
||||
constructor(
|
||||
matchRequest: MatchRequest,
|
||||
defaultParams: IDefaultParams,
|
||||
georouterCreator: ICreateGeorouter,
|
||||
) {
|
||||
this._matchRequest = matchRequest;
|
||||
this._defaultParams = defaultParams;
|
||||
this._georouterCreator = georouterCreator;
|
||||
this._setPerson();
|
||||
this._setRoles();
|
||||
this._setTime();
|
||||
this._setGeography();
|
||||
this._setRequirement();
|
||||
this._setAlgorithmSettings();
|
||||
this._setExclusions();
|
||||
}
|
||||
|
||||
createRoutes = (): void => {
|
||||
this.geography.createRoutes(this.roles, this.algorithmSettings.georouter);
|
||||
};
|
||||
|
||||
_setPerson = (): void => {
|
||||
this.person = new Person(
|
||||
this._matchRequest,
|
||||
this._defaultParams.DEFAULT_IDENTIFIER,
|
||||
this._defaultParams.MARGIN_DURATION,
|
||||
);
|
||||
this.person.init();
|
||||
};
|
||||
|
||||
_setRoles = (): void => {
|
||||
this.roles = [];
|
||||
if (this._matchRequest.driver) this.roles.push(Role.DRIVER);
|
||||
if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER);
|
||||
if (this.roles.length == 0) this.roles.push(Role.PASSENGER);
|
||||
};
|
||||
|
||||
_setTime = (): void => {
|
||||
this.time = new Time(
|
||||
this._matchRequest,
|
||||
this._defaultParams.MARGIN_DURATION,
|
||||
this._defaultParams.VALIDITY_DURATION,
|
||||
);
|
||||
this.time.init();
|
||||
};
|
||||
|
||||
_setGeography = (): void => {
|
||||
this.geography = new Geography(
|
||||
this._matchRequest,
|
||||
this._defaultParams.DEFAULT_TIMEZONE,
|
||||
this.person,
|
||||
);
|
||||
this.geography.init();
|
||||
};
|
||||
|
||||
_setRequirement = (): void => {
|
||||
this.requirement = new Requirement(
|
||||
this._matchRequest,
|
||||
this._defaultParams.DEFAULT_SEATS,
|
||||
);
|
||||
};
|
||||
|
||||
_setAlgorithmSettings = (): void => {
|
||||
this.algorithmSettings = new AlgorithmSettings(
|
||||
this._matchRequest,
|
||||
this._defaultParams.DEFAULT_ALGORITHM_SETTINGS,
|
||||
this.time.frequency,
|
||||
this._georouterCreator,
|
||||
);
|
||||
};
|
||||
|
||||
_setExclusions = (): void => {
|
||||
this.exclusions = [];
|
||||
if (this._matchRequest.identifier)
|
||||
this.exclusions.push(this._matchRequest.identifier);
|
||||
if (this._matchRequest.exclusions)
|
||||
this.exclusions.push(...this._matchRequest.exclusions);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider';
|
||||
import { IDefaultParams } from '../../../../domain/types/default-params.type';
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockImplementationOnce(() => 99),
|
||||
};
|
||||
|
||||
describe('DefaultParamsProvider', () => {
|
||||
let defaultParamsProvider: DefaultParamsProvider;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
DefaultParamsProvider,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
defaultParamsProvider = module.get<DefaultParamsProvider>(
|
||||
DefaultParamsProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(defaultParamsProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide default params', async () => {
|
||||
const params: IDefaultParams = defaultParamsProvider.getParams();
|
||||
expect(params.DEFAULT_IDENTIFIER).toBe(99);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
|
||||
|
||||
describe('Matcher geodesic', () => {
|
||||
it('should be defined', () => {
|
||||
const geodesic: MatcherGeodesic = new MatcherGeodesic();
|
||||
expect(geodesic).toBeDefined();
|
||||
});
|
||||
it('should get inverse values', () => {
|
||||
const geodesic: MatcherGeodesic = new MatcherGeodesic();
|
||||
const inv = geodesic.inverse(0, 0, 1, 1);
|
||||
expect(Math.round(inv.azimuth)).toBe(45);
|
||||
expect(Math.round(inv.distance)).toBe(156900);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator';
|
||||
import { GraphhopperGeorouter } from '../../../../adapters/secondaries/graphhopper-georouter';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
|
||||
|
||||
const mockHttpService = jest.fn();
|
||||
const mockMatcherGeodesic = jest.fn();
|
||||
|
||||
describe('Georouter creator', () => {
|
||||
let georouterCreator: GeorouterCreator;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
GeorouterCreator,
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: mockHttpService,
|
||||
},
|
||||
{
|
||||
provide: MatcherGeodesic,
|
||||
useValue: mockMatcherGeodesic,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(georouterCreator).toBeDefined();
|
||||
});
|
||||
it('should create a graphhopper georouter', () => {
|
||||
const georouter = georouterCreator.create(
|
||||
'graphhopper',
|
||||
'http://localhost',
|
||||
);
|
||||
expect(georouter).toBeInstanceOf(GraphhopperGeorouter);
|
||||
});
|
||||
it('should throw an exception if georouter type is unknown', () => {
|
||||
expect(() =>
|
||||
georouterCreator.create('unknown', 'http://localhost'),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,456 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator';
|
||||
import { IGeorouter } from '../../../../domain/interfaces/georouter.interface';
|
||||
import { of } from 'rxjs';
|
||||
import { AxiosError } from 'axios';
|
||||
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
|
||||
|
||||
const mockHttpService = {
|
||||
get: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new AxiosError('Axios error !');
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return of({
|
||||
status: 200,
|
||||
data: {
|
||||
paths: [
|
||||
{
|
||||
distance: 50000,
|
||||
time: 1800000,
|
||||
snapped_waypoints: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return of({
|
||||
status: 200,
|
||||
data: {
|
||||
paths: [
|
||||
{
|
||||
distance: 50000,
|
||||
time: 1800000,
|
||||
points: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[5, 5],
|
||||
[6, 6],
|
||||
[7, 7],
|
||||
[8, 8],
|
||||
[9, 9],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
snapped_waypoints: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return of({
|
||||
status: 200,
|
||||
data: {
|
||||
paths: [
|
||||
{
|
||||
distance: 50000,
|
||||
time: 1800000,
|
||||
points: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[5, 5],
|
||||
[6, 6],
|
||||
[7, 7],
|
||||
[8, 8],
|
||||
[9, 9],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
details: {
|
||||
time: [
|
||||
[0, 1, 180000],
|
||||
[1, 2, 180000],
|
||||
[2, 3, 180000],
|
||||
[3, 4, 180000],
|
||||
[4, 5, 180000],
|
||||
[5, 6, 180000],
|
||||
[6, 7, 180000],
|
||||
[7, 9, 360000],
|
||||
[9, 10, 180000],
|
||||
],
|
||||
},
|
||||
snapped_waypoints: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return of({
|
||||
status: 200,
|
||||
data: {
|
||||
paths: [
|
||||
{
|
||||
distance: 50000,
|
||||
time: 1800000,
|
||||
points: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[7, 7],
|
||||
[8, 8],
|
||||
[9, 9],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
snapped_waypoints: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[5, 5],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
details: {
|
||||
time: [
|
||||
[0, 1, 180000],
|
||||
[1, 2, 180000],
|
||||
[2, 3, 180000],
|
||||
[3, 4, 180000],
|
||||
[4, 7, 540000],
|
||||
[7, 9, 360000],
|
||||
[9, 10, 180000],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return of({
|
||||
status: 200,
|
||||
data: {
|
||||
paths: [
|
||||
{
|
||||
distance: 50000,
|
||||
time: 1800000,
|
||||
points: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[5, 5],
|
||||
[6, 6],
|
||||
[7, 7],
|
||||
[8, 8],
|
||||
[9, 9],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
snapped_waypoints: {
|
||||
coordinates: [
|
||||
[0, 0],
|
||||
[5, 5],
|
||||
[10, 10],
|
||||
],
|
||||
},
|
||||
details: {
|
||||
time: [
|
||||
[0, 1, 180000],
|
||||
[1, 2, 180000],
|
||||
[2, 3, 180000],
|
||||
[3, 4, 180000],
|
||||
[4, 7, 540000],
|
||||
[7, 9, 360000],
|
||||
[9, 10, 180000],
|
||||
],
|
||||
},
|
||||
instructions: [
|
||||
{
|
||||
distance: 25000,
|
||||
sign: 0,
|
||||
interval: [0, 5],
|
||||
text: 'Some instructions',
|
||||
time: 900000,
|
||||
},
|
||||
{
|
||||
distance: 0,
|
||||
sign: 5,
|
||||
interval: [5, 5],
|
||||
text: 'Waypoint 1',
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
distance: 25000,
|
||||
sign: 2,
|
||||
interval: [5, 10],
|
||||
text: 'Some instructions',
|
||||
time: 900000,
|
||||
},
|
||||
{
|
||||
distance: 0.0,
|
||||
sign: 4,
|
||||
interval: [10, 10],
|
||||
text: 'Arrive at destination',
|
||||
time: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
const mockMatcherGeodesic = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
inverse: jest.fn().mockImplementation(() => ({
|
||||
azimuth: 45,
|
||||
distance: 50000,
|
||||
})),
|
||||
};
|
||||
|
||||
describe('Graphhopper Georouter', () => {
|
||||
let georouterCreator: GeorouterCreator;
|
||||
let graphhopperGeorouter: IGeorouter;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
GeorouterCreator,
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: mockHttpService,
|
||||
},
|
||||
{
|
||||
provide: MatcherGeodesic,
|
||||
useValue: mockMatcherGeodesic,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
|
||||
graphhopperGeorouter = georouterCreator.create(
|
||||
'graphhopper',
|
||||
'http://localhost',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(graphhopperGeorouter).toBeDefined();
|
||||
});
|
||||
|
||||
describe('route function', () => {
|
||||
it('should fail on axios error', async () => {
|
||||
await expect(
|
||||
graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 1,
|
||||
lon: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: false,
|
||||
withPoints: false,
|
||||
withTime: false,
|
||||
},
|
||||
),
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should create one route with all settings to false', async () => {
|
||||
const routes = await graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: false,
|
||||
withPoints: false,
|
||||
withTime: false,
|
||||
},
|
||||
);
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0].route.distance).toBe(50000);
|
||||
});
|
||||
|
||||
it('should create one route with points', async () => {
|
||||
const routes = await graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: false,
|
||||
withPoints: true,
|
||||
withTime: false,
|
||||
},
|
||||
);
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0].route.distance).toBe(50000);
|
||||
expect(routes[0].route.duration).toBe(1800);
|
||||
expect(routes[0].route.fwdAzimuth).toBe(45);
|
||||
expect(routes[0].route.backAzimuth).toBe(225);
|
||||
expect(routes[0].route.points.length).toBe(11);
|
||||
});
|
||||
|
||||
it('should create one route with points and time', async () => {
|
||||
const routes = await graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: false,
|
||||
withPoints: true,
|
||||
withTime: true,
|
||||
},
|
||||
);
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0].route.spacetimePoints.length).toBe(2);
|
||||
expect(routes[0].route.spacetimePoints[1].duration).toBe(1800);
|
||||
expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create one route with points and missed waypoints extrapolations', async () => {
|
||||
const routes = await graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 5,
|
||||
lon: 5,
|
||||
},
|
||||
{
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: false,
|
||||
withPoints: true,
|
||||
withTime: true,
|
||||
},
|
||||
);
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0].route.spacetimePoints.length).toBe(3);
|
||||
expect(routes[0].route.distance).toBe(50000);
|
||||
expect(routes[0].route.duration).toBe(1800);
|
||||
expect(routes[0].route.fwdAzimuth).toBe(45);
|
||||
expect(routes[0].route.backAzimuth).toBe(225);
|
||||
expect(routes[0].route.points.length).toBe(9);
|
||||
});
|
||||
|
||||
it('should create one route with points, time and distance', async () => {
|
||||
const routes = await graphhopperGeorouter.route(
|
||||
[
|
||||
{
|
||||
key: 'route1',
|
||||
points: [
|
||||
{
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
lat: 10,
|
||||
lon: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
withDistance: true,
|
||||
withPoints: true,
|
||||
withTime: true,
|
||||
},
|
||||
);
|
||||
expect(routes).toHaveLength(1);
|
||||
expect(routes[0].route.spacetimePoints.length).toBe(3);
|
||||
expect(routes[0].route.spacetimePoints[1].duration).toBe(990);
|
||||
expect(routes[0].route.spacetimePoints[1].distance).toBe(25000);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../../../adapters/secondaries/messager';
|
||||
|
||||
const mockAmqpConnection = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockResolvedValue({
|
||||
RMQ_EXCHANGE: 'mobicoop',
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Messager', () => {
|
||||
let messager: Messager;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
Messager,
|
||||
{
|
||||
provide: AmqpConnection,
|
||||
useValue: mockAmqpConnection,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
messager = module.get<Messager>(Messager);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(messager).toBeDefined();
|
||||
});
|
||||
|
||||
it('should publish a message', async () => {
|
||||
jest.spyOn(mockAmqpConnection, 'publish');
|
||||
messager.publish('test.create.info', 'my-test');
|
||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,282 @@
|
|||
import { Person } from '../../../../domain/entities/ecosystem/person';
|
||||
import {
|
||||
Geography,
|
||||
RouteKey,
|
||||
} from '../../../../domain/entities/ecosystem/geography';
|
||||
import { Role } from '../../../../domain/types/role.enum';
|
||||
import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route';
|
||||
import { Route } from '../../../../domain/entities/ecosystem/route';
|
||||
import { IGeodesic } from '../../../../domain/interfaces/geodesic.interface';
|
||||
import { PointType } from '../../../../domain/types/geography.enum';
|
||||
|
||||
const person: Person = new Person(
|
||||
{
|
||||
identifier: 1,
|
||||
},
|
||||
0,
|
||||
900,
|
||||
);
|
||||
|
||||
const mockGeodesic: IGeodesic = {
|
||||
inverse: jest.fn().mockImplementation(() => ({
|
||||
azimuth: 45,
|
||||
distance: 50000,
|
||||
})),
|
||||
};
|
||||
|
||||
const mockGeorouter = {
|
||||
route: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return [
|
||||
<NamedRoute>{
|
||||
key: RouteKey.COMMON,
|
||||
route: new Route(mockGeodesic),
|
||||
},
|
||||
];
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return [
|
||||
<NamedRoute>{
|
||||
key: RouteKey.DRIVER,
|
||||
route: new Route(mockGeodesic),
|
||||
},
|
||||
<NamedRoute>{
|
||||
key: RouteKey.PASSENGER,
|
||||
route: new Route(mockGeodesic),
|
||||
},
|
||||
];
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return [
|
||||
<NamedRoute>{
|
||||
key: RouteKey.DRIVER,
|
||||
route: new Route(mockGeodesic),
|
||||
},
|
||||
];
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return [
|
||||
<NamedRoute>{
|
||||
key: RouteKey.PASSENGER,
|
||||
route: new Route(mockGeodesic),
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Geography entity', () => {
|
||||
it('should be defined', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
expect(geography).toBeDefined();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize a geography request with point types', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
type: PointType.LOCALITY,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
type: PointType.LOCALITY,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
geography.init();
|
||||
expect(geography._points.length).toBe(2);
|
||||
expect(geography.originType).toBe(PointType.LOCALITY);
|
||||
expect(geography.destinationType).toBe(PointType.LOCALITY);
|
||||
});
|
||||
it('should throw an exception if waypoints are empty', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
expect(() => geography.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if only one waypoint is provided', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
expect(() => geography.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if a waypoint has invalid longitude', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 201.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
expect(() => geography.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if a waypoint has invalid latitude', () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 250.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
expect(() => geography.init()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create route', () => {
|
||||
it('should create routes as driver and passenger', async () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
geography.init();
|
||||
await geography.createRoutes(
|
||||
[Role.DRIVER, Role.PASSENGER],
|
||||
mockGeorouter,
|
||||
);
|
||||
expect(geography.driverRoute.waypoints.length).toBe(2);
|
||||
expect(geography.passengerRoute.waypoints.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should create routes as driver and passenger with 3 waypoints', async () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 49.781215,
|
||||
lon: 2.198475,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
geography.init();
|
||||
await geography.createRoutes(
|
||||
[Role.DRIVER, Role.PASSENGER],
|
||||
mockGeorouter,
|
||||
);
|
||||
expect(geography.driverRoute.waypoints.length).toBe(3);
|
||||
expect(geography.passengerRoute.waypoints.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should create routes as driver', async () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
geography.init();
|
||||
await geography.createRoutes([Role.DRIVER], mockGeorouter);
|
||||
expect(geography.driverRoute.waypoints.length).toBe(2);
|
||||
expect(geography.passengerRoute).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create routes as passenger', async () => {
|
||||
const geography = new Geography(
|
||||
{
|
||||
waypoints: [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
],
|
||||
},
|
||||
'Europe/Paris',
|
||||
person,
|
||||
);
|
||||
geography.init();
|
||||
await geography.createRoutes([Role.PASSENGER], mockGeorouter);
|
||||
expect(geography.passengerRoute.waypoints.length).toBe(2);
|
||||
expect(geography.driverRoute).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { Person } from '../../../../domain/entities/ecosystem/person';
|
||||
|
||||
const DEFAULT_IDENTIFIER = 0;
|
||||
const MARGIN_DURATION = 900;
|
||||
|
||||
describe('Person entity', () => {
|
||||
it('should be defined', () => {
|
||||
const person = new Person(
|
||||
{
|
||||
identifier: 1,
|
||||
},
|
||||
DEFAULT_IDENTIFIER,
|
||||
MARGIN_DURATION,
|
||||
);
|
||||
expect(person).toBeDefined();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize a person with an identifier', () => {
|
||||
const person = new Person(
|
||||
{
|
||||
identifier: 1,
|
||||
},
|
||||
DEFAULT_IDENTIFIER,
|
||||
MARGIN_DURATION,
|
||||
);
|
||||
person.init();
|
||||
expect(person.identifier).toBe(1);
|
||||
expect(person.marginDurations[0]).toBe(900);
|
||||
expect(person.marginDurations[6]).toBe(900);
|
||||
});
|
||||
it('should initialize a person without an identifier', () => {
|
||||
const person = new Person({}, DEFAULT_IDENTIFIER, MARGIN_DURATION);
|
||||
person.init();
|
||||
expect(person.identifier).toBe(0);
|
||||
expect(person.marginDurations[0]).toBe(900);
|
||||
expect(person.marginDurations[6]).toBe(900);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import { Route } from '../../../../domain/entities/ecosystem/route';
|
||||
import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point';
|
||||
import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint';
|
||||
|
||||
const mockGeodesic = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => {
|
||||
return lon1 == 0
|
||||
? {
|
||||
azimuth: 45,
|
||||
distance: 50000,
|
||||
}
|
||||
: {
|
||||
azimuth: -45,
|
||||
distance: 60000,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
describe('Route entity', () => {
|
||||
it('should be defined', () => {
|
||||
const route = new Route(mockGeodesic);
|
||||
expect(route).toBeDefined();
|
||||
});
|
||||
it('should set waypoints and geodesic values for a route', () => {
|
||||
const route = new Route(mockGeodesic);
|
||||
const waypoint1: Waypoint = new Waypoint({
|
||||
lon: 0,
|
||||
lat: 0,
|
||||
});
|
||||
const waypoint2: Waypoint = new Waypoint({
|
||||
lon: 10,
|
||||
lat: 10,
|
||||
});
|
||||
route.setWaypoints([waypoint1, waypoint2]);
|
||||
expect(route.waypoints.length).toBe(2);
|
||||
expect(route.fwdAzimuth).toBe(45);
|
||||
expect(route.backAzimuth).toBe(225);
|
||||
expect(route.distanceAzimuth).toBe(50000);
|
||||
});
|
||||
it('should set points and geodesic values for a route', () => {
|
||||
const route = new Route(mockGeodesic);
|
||||
route.setPoints([
|
||||
{
|
||||
lon: 10,
|
||||
lat: 10,
|
||||
},
|
||||
{
|
||||
lon: 20,
|
||||
lat: 20,
|
||||
},
|
||||
]);
|
||||
expect(route.points.length).toBe(2);
|
||||
expect(route.fwdAzimuth).toBe(315);
|
||||
expect(route.backAzimuth).toBe(135);
|
||||
expect(route.distanceAzimuth).toBe(60000);
|
||||
});
|
||||
it('should set spacetimePoints for a route', () => {
|
||||
const route = new Route(mockGeodesic);
|
||||
const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0);
|
||||
const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000);
|
||||
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);
|
||||
expect(route.spacetimePoints.length).toBe(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
import { Time } from '../../../../domain/entities/ecosystem/time';
|
||||
|
||||
const MARGIN_DURATION = 900;
|
||||
const VALIDITY_DURATION = 365;
|
||||
|
||||
describe('Time entity', () => {
|
||||
it('should be defined', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(time).toBeDefined();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize a punctual time request', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
time.init();
|
||||
expect(time.fromDate.getFullYear()).toBe(
|
||||
new Date('2023-04-01 12:24:00').getFullYear(),
|
||||
);
|
||||
});
|
||||
it('should initialize a punctual time request with specific single margin duration', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
marginDuration: 300,
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
time.init();
|
||||
expect(time.marginDurations['tue']).toBe(300);
|
||||
});
|
||||
it('should initialize a punctual time request with specific margin durations', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
marginDurations: {
|
||||
sat: 350,
|
||||
},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
time.init();
|
||||
expect(time.marginDurations['tue']).toBe(900);
|
||||
expect(time.marginDurations['sat']).toBe(350);
|
||||
});
|
||||
it('should initialize a punctual time request with specific single margin duration and margin durations', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
marginDuration: 500,
|
||||
marginDurations: {
|
||||
sat: 350,
|
||||
},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
time.init();
|
||||
expect(time.marginDurations['tue']).toBe(500);
|
||||
expect(time.marginDurations['sat']).toBe(350);
|
||||
});
|
||||
it('should initialize a recurrent time request', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
schedule: {
|
||||
mon: '12:00',
|
||||
},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
time.init();
|
||||
expect(time.fromDate.getFullYear()).toBe(
|
||||
new Date('2023-04-01').getFullYear(),
|
||||
);
|
||||
});
|
||||
it('should throw an exception if no date is provided', () => {
|
||||
const time = new Time({}, MARGIN_DURATION, VALIDITY_DURATION);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if punctual date is invalid', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-15-01 12:24:00',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if recurrent fromDate is invalid', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-15-01',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if recurrent toDate is invalid', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
toDate: '2023-13-01',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if recurrent toDate is before fromDate', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
toDate: '2023-03-01',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if schedule is missing', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
toDate: '2024-03-31',
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if schedule is empty', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
toDate: '2024-03-31',
|
||||
schedule: {},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
it('should throw an exception if schedule is invalid', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
fromDate: '2023-04-01',
|
||||
toDate: '2024-03-31',
|
||||
schedule: {
|
||||
mon: '32:78',
|
||||
},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
});
|
||||
it('should throw an exception if margin durations is provided but empty', () => {
|
||||
const time = new Time(
|
||||
{
|
||||
departure: '2023-04-01 12:24:00',
|
||||
marginDurations: {},
|
||||
},
|
||||
MARGIN_DURATION,
|
||||
VALIDITY_DURATION,
|
||||
);
|
||||
expect(() => time.init()).toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Messager } from '../../../adapters/secondaries/messager';
|
||||
import { MatchUseCase } from '../../../domain/usecases/match.usecase';
|
||||
import { MatchRequest } from '../../../domain/dtos/match.request';
|
||||
import { MatchQuery } from '../../../queries/match.query';
|
||||
import { AdRepository } from '../../../adapters/secondaries/ad.repository';
|
||||
import { AutomapperModule } from '@automapper/nestjs';
|
||||
import { classes } from '@automapper/classes';
|
||||
import { IDefaultParams } from '../../../domain/types/default-params.type';
|
||||
import { Algorithm } from '../../../domain/types/algorithm.enum';
|
||||
|
||||
const mockAdRepository = {};
|
||||
|
||||
const mockMessager = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockGeorouterCreator = {
|
||||
create: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const defaultParams: IDefaultParams = {
|
||||
DEFAULT_IDENTIFIER: 0,
|
||||
MARGIN_DURATION: 900,
|
||||
VALIDITY_DURATION: 365,
|
||||
DEFAULT_TIMEZONE: 'Europe/Paris',
|
||||
DEFAULT_SEATS: 3,
|
||||
DEFAULT_ALGORITHM_SETTINGS: {
|
||||
algorithm: Algorithm.CLASSIC,
|
||||
strict: false,
|
||||
remoteness: 15000,
|
||||
useProportion: true,
|
||||
proportion: 0.3,
|
||||
useAzimuth: true,
|
||||
azimuthMargin: 10,
|
||||
maxDetourDistanceRatio: 0.3,
|
||||
maxDetourDurationRatio: 0.3,
|
||||
georouterType: 'graphhopper',
|
||||
georouterUrl: 'http://localhost',
|
||||
},
|
||||
};
|
||||
|
||||
describe('MatchUseCase', () => {
|
||||
let matchUseCase: MatchUseCase;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
||||
providers: [
|
||||
{
|
||||
provide: AdRepository,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
{
|
||||
provide: Messager,
|
||||
useValue: mockMessager,
|
||||
},
|
||||
MatchUseCase,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
matchUseCase = module.get<MatchUseCase>(MatchUseCase);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(matchUseCase).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should return matches', async () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lon: 1.093912,
|
||||
lat: 49.440041,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.departure = '2023-04-01 12:23:00';
|
||||
const matches = await matchUseCase.execute(
|
||||
new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator),
|
||||
);
|
||||
expect(matches.total).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,210 @@
|
|||
import { MatchRequest } from '../../../domain/dtos/match.request';
|
||||
import { Role } from '../../../domain/types/role.enum';
|
||||
import { TimingFrequency } from '../../../domain/types/timing';
|
||||
import { IDefaultParams } from '../../../domain/types/default-params.type';
|
||||
import { MatchQuery } from '../../../queries/match.query';
|
||||
import { Algorithm } from '../../../domain/types/algorithm.enum';
|
||||
|
||||
const defaultParams: IDefaultParams = {
|
||||
DEFAULT_IDENTIFIER: 0,
|
||||
MARGIN_DURATION: 900,
|
||||
VALIDITY_DURATION: 365,
|
||||
DEFAULT_TIMEZONE: 'Europe/Paris',
|
||||
DEFAULT_SEATS: 3,
|
||||
DEFAULT_ALGORITHM_SETTINGS: {
|
||||
algorithm: Algorithm.CLASSIC,
|
||||
strict: false,
|
||||
remoteness: 15000,
|
||||
useProportion: true,
|
||||
proportion: 0.3,
|
||||
useAzimuth: true,
|
||||
azimuthMargin: 10,
|
||||
maxDetourDistanceRatio: 0.3,
|
||||
maxDetourDurationRatio: 0.3,
|
||||
georouterType: 'graphhopper',
|
||||
georouterUrl: 'http://localhost',
|
||||
},
|
||||
};
|
||||
|
||||
const mockGeorouterCreator = {
|
||||
create: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('Match query', () => {
|
||||
it('should be defined', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a query with excluded identifiers', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.identifier = 125;
|
||||
matchRequest.exclusions = [126, 127, 128];
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.exclusions.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should create a query with driver role only', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.driver = true;
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.roles).toEqual([Role.DRIVER]);
|
||||
});
|
||||
|
||||
it('should create a query with passenger role only', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.passenger = true;
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.roles).toEqual([Role.PASSENGER]);
|
||||
});
|
||||
|
||||
it('should create a query with driver and passenger roles', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.passenger = true;
|
||||
matchRequest.driver = true;
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.roles.length).toBe(2);
|
||||
expect(matchQuery.roles).toContain(Role.PASSENGER);
|
||||
expect(matchQuery.roles).toContain(Role.DRIVER);
|
||||
});
|
||||
|
||||
it('should create a query with number of seats modified', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.seatsDriver = 1;
|
||||
matchRequest.seatsPassenger = 2;
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.requirement.seatsDriver).toBe(1);
|
||||
expect(matchQuery.requirement.seatsPassenger).toBe(2);
|
||||
});
|
||||
|
||||
it('should create a query with modified algorithm settings', () => {
|
||||
const matchRequest: MatchRequest = new MatchRequest();
|
||||
matchRequest.departure = '2023-04-01 12:00';
|
||||
matchRequest.waypoints = [
|
||||
{
|
||||
lat: 49.440041,
|
||||
lon: 1.093912,
|
||||
},
|
||||
{
|
||||
lat: 50.630992,
|
||||
lon: 3.045432,
|
||||
},
|
||||
];
|
||||
matchRequest.algorithm = Algorithm.CLASSIC;
|
||||
matchRequest.strict = true;
|
||||
matchRequest.useProportion = true;
|
||||
matchRequest.proportion = 0.45;
|
||||
matchRequest.useAzimuth = true;
|
||||
matchRequest.azimuthMargin = 15;
|
||||
matchRequest.remoteness = 20000;
|
||||
matchRequest.maxDetourDistanceRatio = 0.41;
|
||||
matchRequest.maxDetourDurationRatio = 0.42;
|
||||
const matchQuery: MatchQuery = new MatchQuery(
|
||||
matchRequest,
|
||||
defaultParams,
|
||||
mockGeorouterCreator,
|
||||
);
|
||||
expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC);
|
||||
expect(matchQuery.algorithmSettings.restrict).toBe(
|
||||
TimingFrequency.FREQUENCY_PUNCTUAL,
|
||||
);
|
||||
expect(matchQuery.algorithmSettings.useProportion).toBeTruthy();
|
||||
expect(matchQuery.algorithmSettings.proportion).toBe(0.45);
|
||||
expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy();
|
||||
expect(matchQuery.algorithmSettings.azimuthMargin).toBe(15);
|
||||
expect(matchQuery.algorithmSettings.remoteness).toBe(20000);
|
||||
expect(matchQuery.algorithmSettings.maxDetourDistanceRatio).toBe(0.41);
|
||||
expect(matchQuery.algorithmSettings.maxDetourDurationRatio).toBe(0.42);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { ArgumentMetadata } from '@nestjs/common';
|
||||
import { UpdateTerritoryRequest } from '../../../modules/territory/domain/dtos/update-territory.request';
|
||||
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
|
||||
import { MatchRequest } from '../../../matcher/domain/dtos/match.request';
|
||||
|
||||
describe('RpcValidationPipe', () => {
|
||||
it('should not validate request', async () => {
|
||||
|
@ -10,13 +10,11 @@ describe('RpcValidationPipe', () => {
|
|||
});
|
||||
const metadata: ArgumentMetadata = {
|
||||
type: 'body',
|
||||
metatype: UpdateTerritoryRequest,
|
||||
metatype: MatchRequest,
|
||||
data: '',
|
||||
};
|
||||
await target
|
||||
.transform(<UpdateTerritoryRequest>{}, metadata)
|
||||
.catch((err) => {
|
||||
expect(err.message).toEqual('Rpc Exception');
|
||||
});
|
||||
await target.transform(<MatchRequest>{}, metadata).catch((err) => {
|
||||
expect(err.message).toEqual('Rpc Exception');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue