From e39bce59ba91a85585e0adbcd50a124778d9555c Mon Sep 17 00:00:00 2001 From: Robin Ferch Date: Mon, 23 Jan 2023 22:40:22 +0100 Subject: [PATCH] :sparkles: feat(search): Add search functionalities --- backend/.example.env | 4 + backend/package.json | 1 + backend/prisma/schema.prisma | 1 + backend/src/Core.ts | 8 +- backend/src/controllers/AdminController.ts | 97 +++++++++++++++++++++- backend/src/util/SearchController.ts | 32 +++++++ backend/src/web/routes/index.ts | 15 +++- frontend/.env | 3 + frontend/package.json | 1 + frontend/src/components/AdminGeneral.jsx | 97 ++++++++++++++++++---- frontend/src/components/Map.jsx | 46 +++++++--- frontend/src/components/RegionView.jsx | 15 ++++ frontend/src/utils/SearchEngine.jsx | 33 ++++++-- frontend/yarn.lock | 39 +++++++++ 14 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 backend/src/util/SearchController.ts diff --git a/backend/.example.env b/backend/.example.env index a9c7de0..003d9ed 100644 --- a/backend/.example.env +++ b/backend/.example.env @@ -22,3 +22,7 @@ S3_HOST=https://s3.yourhost.com S3_ACCESS_KEY=accesskeyaccesskeyaccesskeyaccesskey S3_SECRET_KEY=secretkeysecretkeysecretkeysecretkey S3_BUCKET=yourbucket + +MEILISEARCH_HOST=https://search.yourhost.com +MEILISEARCH_KEY=tokentokentokentokentokentokentokentokentoken +MEILISEARCH_INDEX=map diff --git a/backend/package.json b/backend/package.json index 54e6c1a..e231e54 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,6 +46,7 @@ "jsonwebtoken": "^8.5.1", "keycloak-connect": "^17.0.0", "log4js": "^6.3.0", + "meilisearch": "^0.30.0", "minio": "^7.0.32", "reflect-metadata": "^0.1.13", "rfdc": "^1.3.0", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 233b720..1d4c0c3 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -26,6 +26,7 @@ model Region { isEventRegion Boolean @default(false) isPlotRegion Boolean @default(false) buildings Int @default(0) + osmDisplayName String @default("") } model User { diff --git a/backend/src/Core.ts b/backend/src/Core.ts index cf6b792..e47bf19 100644 --- a/backend/src/Core.ts +++ b/backend/src/Core.ts @@ -14,6 +14,8 @@ import KeycloakAdmin from "./util/KeycloakAdmin"; import {PrismaClient} from "@prisma/client"; import DiscordIntegration from "./util/DiscordIntegration"; import S3Controller from "./util/S3Controller"; +import SearchController from "./util/SearchController"; +import {MeiliSearch} from "meilisearch"; class Core { web: Web; @@ -24,6 +26,8 @@ class Core { discord: DiscordIntegration; s3: S3Controller; + search: SearchController; + constructor() { this.setUpLogger(); @@ -40,6 +44,7 @@ class Core { }) this.discord = new DiscordIntegration(this); this.s3 = new S3Controller(this); + this.search = new SearchController(this); } @@ -54,7 +59,8 @@ class Core { public getPrisma = (): PrismaClient => this.prisma; public getDiscord = (): DiscordIntegration => this.discord; public getWeb = (): Web => this.web; - public getS3 = (): S3Controller => this.s3; + public getS3 = (): S3Controller => this.s3.getMinioInstance(); + public getSearch = (): MeiliSearch => this.search.getMeiliInstance(); } diff --git a/backend/src/controllers/AdminController.ts b/backend/src/controllers/AdminController.ts index b5ced40..95e4f9c 100644 --- a/backend/src/controllers/AdminController.ts +++ b/backend/src/controllers/AdminController.ts @@ -9,15 +9,18 @@ import Core from "../Core"; import axios from "axios"; import {response} from "express"; +import {centerOfMass, polygon} from "@turf/turf"; class AdminController { private core: Core; private reCalcProgress: number; + private osmDisplayNameProgress: number; constructor(core: Core) { this.core = core; this.reCalcProgress = 0; + this.osmDisplayNameProgress = 0; } public async getAllUsers(req, res) { @@ -130,9 +133,101 @@ class AdminController { this.reCalcProgress = 0; } - public async getCalculationProgess(req, res) { + public async getCalculationProgress(req, res) { res.send(this.reCalcProgress.toString()); } + + public async getOsmDisplayNames(req, res) { + if (this.osmDisplayNameProgress > 0) { + response.send("Already started.") + return; + } + let regions = await this.core.getPrisma().region.findMany(); + res.send({status: "ok", count: regions.length}) + + for (const [i, region] of regions.entries()) { + if (req.query?.skipOld === "true" && region.osmDisplayName !== "") { + continue; + } + let coords = JSON.parse(region.data); + coords.push(coords[0]); + let poly = polygon([coords]); + let centerMass = centerOfMass(poly); + let center = centerMass.geometry.coordinates; + try { + const {data} = await axios.get(`https://nominatim.openstreetmap.org/reverse?lat=${center[0]}&lon=${center[1]}&format=json&accept-language=de`, {headers: {'User-Agent': 'BTEMAP/1.0'}}); + this.core.getLogger().debug(`Got data for ${region.id}`) + if (data?.display_name) { + await this.core.getPrisma().region.update({ + where: { + id: region.id + }, + data: { + osmDisplayName: data.display_name + } + }) + } + } catch (e) { + this.core.getLogger().error(e); + } + + this.osmDisplayNameProgress = i; + + + } + + this.osmDisplayNameProgress = 0; + + } + + + public async getOsmDisplayNameProgress(req, res) { + res.send(this.osmDisplayNameProgress.toString()); + } + + public async syncWithSearchDB(req, res) { + + // reset index + try { + let oldIndex = await this.core.getSearch().getIndex(process.env.MEILISEARCH_INDEX); + if (oldIndex) { + await this.core.getSearch().deleteIndex(process.env.MEILISEARCH_INDEX) + } + } catch (e) { + + } + + + await this.core.getSearch().createIndex(process.env.MEILISEARCH_INDEX); + await this.core.getSearch().index(process.env.MEILISEARCH_INDEX).updateFilterableAttributes(['_geo']); + await this.core.getSearch().index(process.env.MEILISEARCH_INDEX).updateSortableAttributes(['_geo']); + + const regions = await this.core.getPrisma().region.findMany(); + + let formattedRegions = regions.map((region) => { + let coords = JSON.parse(region.data); + coords.push(coords[0]); + let poly = polygon([coords]); + let centerMass = centerOfMass(poly); + let center = centerMass.geometry.coordinates; + return { + id: region.id, + city: region.city, + "_geo": { + "lat": center[0], + "lng": center[1] + }, + osmDisplayName: region.osmDisplayName, + username: region.username + } + }) + + await this.core.getSearch().index(process.env.MEILISEARCH_INDEX).addDocuments(formattedRegions) + + res.send("ok") + + + } } diff --git a/backend/src/util/SearchController.ts b/backend/src/util/SearchController.ts new file mode 100644 index 0000000..fe67088 --- /dev/null +++ b/backend/src/util/SearchController.ts @@ -0,0 +1,32 @@ +/****************************************************************************** + * SearchController.ts * + * * + * Copyright (c) 2022-2023 Robin Ferch * + * https://robinferch.me * + * This project is released under the MIT license. * + ******************************************************************************/ + +import Core from "../Core"; +import {MeiliSearch} from "meilisearch"; + +class S3Controller { + + private core: Core; + + private readonly meiliInstance: MeiliSearch; + + + constructor(core: Core) { + this.core = core; + this.meiliInstance = new MeiliSearch({host: process.env.MEILISEARCH_HOST, apiKey: process.env.MEILISEARCH_KEY}); + this.core.getLogger().debug("Started Search Controller.") + } + + + public getMeiliInstance(): any { + return this.meiliInstance; + } +} + + +export default S3Controller; diff --git a/backend/src/web/routes/index.ts b/backend/src/web/routes/index.ts index 3abff31..658cb2b 100644 --- a/backend/src/web/routes/index.ts +++ b/backend/src/web/routes/index.ts @@ -127,7 +127,20 @@ class Routes { }, this.keycloak.protect("realm:mapadmin"), checkNewUser(this.web.getCore().getPrisma(), this.web.getCore()), body('userId').isString()) router.addRoute(RequestMethods.GET, "/admin/calculateProgress", async (request, response) => { - await adminController.getCalculationProgess(request, response); + await adminController.getCalculationProgress(request, response); + }, this.keycloak.protect("realm:mapadmin"), checkNewUser(this.web.getCore().getPrisma(), this.web.getCore()), body('userId').isString()) + + router.addRoute(RequestMethods.GET, "/admin/getOsmDisplayNames", async (request, response) => { + await adminController.getOsmDisplayNames(request, response); + }, this.keycloak.protect("realm:mapadmin"), checkNewUser(this.web.getCore().getPrisma(), this.web.getCore()), body('userId').isString()) + + router.addRoute(RequestMethods.GET, "/admin/osmDisplayNameProgress", async (request, response) => { + await adminController.getOsmDisplayNameProgress(request, response); + }, this.keycloak.protect("realm:mapadmin"), checkNewUser(this.web.getCore().getPrisma(), this.web.getCore()), body('userId').isString()) + + + router.addRoute(RequestMethods.GET, "/admin/syncWithSearchDB", async (request, response) => { + await adminController.syncWithSearchDB(request, response); }, this.keycloak.protect("realm:mapadmin"), checkNewUser(this.web.getCore().getPrisma(), this.web.getCore()), body('userId').isString()) diff --git a/frontend/.env b/frontend/.env index 3aca571..919d132 100644 --- a/frontend/.env +++ b/frontend/.env @@ -1 +1,4 @@ VITE_WS_HOST=https://map.bte-germany.de +VITE_SEARCH_URL=https://search.bte-germany.de +VITE_SEARCH_KEY=ac0f027dadb439dbf0d4fc7f3e00da433aa872dae2f75417ba01ad0d0e6b0035 +VITE_SEARCH_INDEX=map diff --git a/frontend/package.json b/frontend/package.json index cd09575..cd6a379 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "mantine-datatable": "^1.7.34", "mapbox-gl": "^2.12.0", "mapbox-gl-style-switcher": "^1.0.11", + "meilisearch": "^0.30.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^4.7.1", diff --git a/frontend/src/components/AdminGeneral.jsx b/frontend/src/components/AdminGeneral.jsx index b9a8eed..4ec7f44 100644 --- a/frontend/src/components/AdminGeneral.jsx +++ b/frontend/src/components/AdminGeneral.jsx @@ -7,16 +7,20 @@ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ import React, {useEffect, useState} from 'react'; -import {Button, Checkbox, Progress, Title} from "@mantine/core"; +import {Button, Checkbox, Paper, Progress, Title, Badge, Group, Alert} from "@mantine/core"; import axios from "axios"; import {useKeycloak} from "@react-keycloak-fork/web"; +import {AiFillWarning, AiOutlineWarning, IoWarningOutline} from "react-icons/all"; +import {showNotification} from "@mantine/notifications"; const AdminGeneral = props => { - const [progress, setProgess] = useState(0); + const [progress, setProgress] = useState(0); + const [osmProgress, setOsmProgress] = useState(0); const [allCount, setAllCount] = useState(0); const [allBuildingsCount, setAllBuildingsCount] = useState(0); const [skipOld, setSkipOld] = useState(false); + const [skipOldOsm, setSkipOldOsm] = useState(false); const {keycloak} = useKeycloak(); @@ -24,14 +28,17 @@ const AdminGeneral = props => { let interval; axios.get("/api/v1/stats/general").then(({data: statsData}) => { setAllCount(statsData.regionCount) + setAllBuildingsCount(statsData.totalBuildings) interval = setInterval(async () => { - console.log("test123") const {data: progress} = await axios.get(`/api/v1/admin/calculateProgress`, {headers: {authorization: "Bearer " + keycloak.token}}) + const {data: progressOsm} = await axios.get(`/api/v1/admin/osmDisplayNameProgress`, + {headers: {authorization: "Bearer " + keycloak.token}}) const {data: stats} = await axios.get(`/api/v1/stats/general`) setAllBuildingsCount(stats.totalBuildings) - setProgess(progress / statsData.regionCount * 100) + setProgress(progress / statsData.regionCount * 100) + setOsmProgress(progressOsm / statsData.regionCount * 100) }, 2000) }) @@ -40,7 +47,7 @@ const AdminGeneral = props => { }, []); const start = () => { - setProgess(0.0000000001); + setProgress(0.0000000001); axios.get(`/api/v1/admin/recalculateBuildings${skipOld ? "?skipOld=true" : ""}`, {headers: {authorization: "Bearer " + keycloak.token}}).then(({data}) => { showNotification({ title: "Ok", @@ -49,19 +56,79 @@ const AdminGeneral = props => { }) } + const startOsm = () => { + setOsmProgress(0.0000000001); + axios.get(`/api/v1/admin/getOsmDisplayNames${skipOld ? "?skipOld=true" : ""}`, {headers: {authorization: "Bearer " + keycloak.token}}).then(({data}) => { + showNotification({ + title: "Ok", + message: `Von ${data.count} Regionen werden die OSM Namen geholt.`, + color: "hreen" + }) + }) + } + + const syncSearch = async () => { + showNotification({ + title: 'Ok', + message: 'Synchronisiere Search-DB', + color: "green" + }) + await axios.get(`/api/v1/admin/syncWithSearchDB`, {headers: {authorization: "Bearer " + keycloak.token}}) + showNotification({ + title: 'Fertig', + message: 'Synchronisierung abgeschlossen', + color: "green" + }) + } + return (
- - 0 werden übersprungen)"} mt={"md"} - value={skipOld} onChange={(event) => setSkipOld(event.currentTarget.checked)}/> - { - progress > 0 && - - } - { - Aktuell {allBuildingsCount} Gebäude - } + + + + Buildings + Aktuell {allBuildingsCount} Gebäude + + + + 0 werden übersprungen)"} mt={"md"} + value={skipOld} onChange={(event) => setSkipOld(event.currentTarget.checked)}/> + { + progress > 0 && + + } + + + + + Search + + + + } mt={"sm"}> + Der gesamte Index wird gelöscht und danach neu erstellt! + + + + + + + OSM Display Name + + + + setSkipOldOsm(event.currentTarget.checked)}/> + { + osmProgress > 0 && + + } + +
); diff --git a/frontend/src/components/Map.jsx b/frontend/src/components/Map.jsx index 1a86607..5ccde7d 100644 --- a/frontend/src/components/Map.jsx +++ b/frontend/src/components/Map.jsx @@ -10,8 +10,8 @@ import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} fro import axios from "axios"; import mapboxgl from "mapbox-gl"; import "mapbox-gl/dist/mapbox-gl.css"; -import {Box, Button, Loader, LoadingOverlay} from "@mantine/core"; -import {useClipboard} from "@mantine/hooks"; +import {Box, Loader, LoadingOverlay} from "@mantine/core"; +import {useClipboard, useDebouncedState} from "@mantine/hooks"; import {showNotification} from "@mantine/notifications"; import {BsCheck2} from "react-icons/bs"; import "mapbox-gl-style-switcher/styles.css"; @@ -19,15 +19,12 @@ import {MapboxStyleSwitcherControl} from "mapbox-gl-style-switcher"; import useQuery from "../hooks/useQuery"; import {centerOfMass, polygon} from "@turf/turf"; import {AiOutlineSearch} from "react-icons/ai"; -import {SpotlightProvider, useSpotlight} from "@mantine/spotlight"; +import {SpotlightProvider} from "@mantine/spotlight"; import {BiMapPin} from "react-icons/bi"; -import searchInOSM from "../utils/SearchEngine"; +import {searchInRegions, searchInOSM} from "../utils/SearchEngine"; import socketIOClient from "socket.io-client"; import {TbPlugConnectedX} from "react-icons/tb"; - -import * as THREE from 'three'; -import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; import generate3DLayer from "../utils/generate3DLayer"; @@ -343,6 +340,12 @@ const Map = forwardRef(({openDialog, setRegionViewData, updateMap, setUpdateMap} }); } + const [searchQuery, setSearchQuery] = useDebouncedState('', 200); + + useEffect(() => { + handleQueryChange(searchQuery) + }, [searchQuery]); + const handleQueryChange = (query) => { if (!query) { setActions([]) @@ -362,19 +365,36 @@ const Map = forwardRef(({openDialog, setRegionViewData, updateMap, setUpdateMap} return; } - setShowSearchLoading(true); - searchInOSM(query, changeLatLon).then(r => { - setActions(r); - setShowSearchLoading(false); + + searchInRegions(query, changeLatLon).then(r => { + let finished = r; + searchInOSM(query, changeLatLon).then((r1) => { + console.log("test1234") + r1.forEach((osmResult) => { + finished.push(osmResult) + }) + setActions(finished); + setShowSearchLoading(false); + }) + + }) + } return ( - { + if (query) { + setShowSearchLoading(true); + setSearchQuery(query); + } else { + setShowSearchLoading(false); + } + }} searchIcon={showSearchLoading ? : } - filter={(query, actions) => actions}> + filter={(query, actions) => actions} limit={50}>
{ !socketConnected && diff --git a/frontend/src/components/RegionView.jsx b/frontend/src/components/RegionView.jsx index e0e9398..8da0a0e 100644 --- a/frontend/src/components/RegionView.jsx +++ b/frontend/src/components/RegionView.jsx @@ -392,6 +392,21 @@ const RegionView = ({data, open, setOpen, setUpdateMap}) => { + + OSM display name + + + copyId(region.osmDisplayName)} sx={{ + cursor: "pointer" + }}>{region.osmDisplayName} + + + diff --git a/frontend/src/utils/SearchEngine.jsx b/frontend/src/utils/SearchEngine.jsx index 8e07f2c..46ed75d 100644 --- a/frontend/src/utils/SearchEngine.jsx +++ b/frontend/src/utils/SearchEngine.jsx @@ -1,7 +1,7 @@ /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + SearchEngine.jsx + + + - + Copyright (c) 2022 Robin Ferch + + + Copyright (c) 2022-2023 Robin Ferch + + https://robinferch.me + + This project is released under the MIT license. + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ @@ -11,11 +11,15 @@ import {BiMapPin} from "react-icons/bi"; import axios from "axios"; import {OSMTagTranslations} from "./OSMTagTranslations"; import {getIcon} from "./OSMTagIcons"; +import {MeiliSearch} from "meilisearch"; +import {TbPolygon} from "react-icons/all"; -const searchInOSM = async (query, flyTo) => { +const meilisearch = new MeiliSearch({host: import.meta.env.VITE_SEARCH_URL, apiKey: import.meta.env.VITE_SEARCH_KEY}) + +export const searchInOSM = async (query, flyTo) => { const result = []; const {data} = await axios.get(`https://photon.komoot.io/api/?q=${query}`); - data.features.forEach(feature => { + data.features.slice(0, 5).forEach(feature => { let featureType = feature.properties.osm_key; let tagTranslation = OSMTagTranslations["tag:" + featureType]; let name = feature.properties.name; @@ -42,11 +46,30 @@ const searchInOSM = async (query, flyTo) => { }) }); - console.log(result) return result; } +export const searchInRegions = async (query, flyTo) => { + console.log(query) + const results = await meilisearch.index(import.meta.env.VITE_SEARCH_INDEX).search(query, {limit: 5}) + let end = []; + if (results?.hits) { + end = results.hits.map((region) => { + return { + title: `${region.city}`, + description: `${region.osmDisplayName} by ${region.username}`, + onTrigger: () => flyTo(region._geo.lat, region._geo.lng), + icon: , + group: "Regions", + } + }) + } + + console.log(end) + + return end; +} + -export default searchInOSM; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1f8c25b..cd5e672 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2158,6 +2158,13 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" @@ -2701,6 +2708,13 @@ mapbox-gl@^2.12.0: tinyqueue "^2.0.3" vt-pbf "^3.1.3" +meilisearch@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.30.0.tgz#707f9a6b07440c496b965379616e084f112160ae" + integrity sha512-3y1hALOwTDpquYar+gDREqRasFPWKxkWAhk6h+RF+nKObPVf9N77wcTNvukGwOKbxRyJnKge0OPgAB1BkB9VVw== + dependencies: + cross-fetch "^3.1.5" + memoize-one@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" @@ -2738,6 +2752,13 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-releases@^2.0.6: version "2.0.8" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" @@ -3231,6 +3252,11 @@ topojson-server@3.x: dependencies: commander "2" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + tslib@^2.0.0, tslib@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" @@ -3292,6 +3318,19 @@ vt-pbf@^3.1.1, vt-pbf@^3.1.3: "@mapbox/vector-tile" "^1.3.1" pbf "^3.2.1" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + ws@~8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"