From 97e3f375a1524d956f4f5636e0189bb32b0e804f Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 14 Jun 2024 14:04:34 +0700 Subject: [PATCH] Node script to copy projects from staging or prod (#1816) * Node script to copy projects from staging or prod Often fails to copy assets because `kubectl cp` or `kubectl exec tar` get cut off partway through, but that should go away once Kubernetes version 1.30 is released. * Fetch project assets with rsync and retry failures This should ensure that the project assets eventually get copied over to the local Docker setup even under conditions where `kubectl exec` is flaky and fails every couple of minutes. * Only set up rsync if needed This will save a bit of time when kubectl cp is being reliable * Only include assets that are really there Before including pictures and audio in the tarball, make sure they're really there, and skip them if they are a broken symlink. * Make backup script slightly more cross-platform Windows has issues with single-quotes for quoting command-line params, but thankfully Linux handles double-quotes correctly in all the places I used single-quotes, so we'll just switch to double-quotes everywhere. * Clean up assets tarball when done Also use pod name instead of deploy/app since not every user account has access to deploy objects, at least on production * Rewrite path, to workaround kubectl interpreting Windows drive letter as pod name --------- Co-authored-by: Tim Haasdyk --- backup.mjs | 360 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 104 ++++++++++++++ 3 files changed, 465 insertions(+) create mode 100644 backup.mjs diff --git a/backup.mjs b/backup.mjs new file mode 100644 index 0000000000..b7ee7ea555 --- /dev/null +++ b/backup.mjs @@ -0,0 +1,360 @@ +import { execSync, spawn } from "child_process"; +import { existsSync, mkdtempSync, rmSync, statSync } from "fs"; +import { MongoClient, ObjectId } from "mongodb"; +import os from "os"; +import path from "path"; +import net from "net"; + +// Expected arguments: first arg is project ID (5dbf805650b51914727e06c4) or URL (http://localhost:8080/app/lexicon/5dbf805650b51914727e06c4) +// Second arg is "qa" or "staging" to copy from staging, "live" or "prod" or "production" to copy from production +// NOTE: You must edit the context names below if they don't match the context names you have (see `kubectl config get-contexts` output) + +// ===== EDIT THIS ===== + +const stagingContext = "dallas-rke"; +const prodContext = "aws-rke"; + +// ===== END of EDIT THIS ===== + +let defaultContext = stagingContext; +let defaultContextName = "staging"; + +// Create a temp dir reliably +const tempdir = mkdtempSync(path.join(os.tmpdir(), "lfbackup-")) + // Work around kubectl bug where Windows drive letters are interpreted as pod names by kubectl cp + .replace(/^C:\\/, "\\\\localhost\\C$\\"); +let portForwardProcess; +let localConn; +let remoteConn; +let remoteTarball = undefined; +let remotePodname = undefined; + +async function cleanup() { + try { + if (existsSync(tempdir)) { + console.warn(`Cleaning up temporary directory ${tempdir}...`); + rmSync(tempdir, { recursive: true, force: true }); + } + } catch (_) {} + try { + if (remotePodname && remoteTarball) { + console.warn(`Cleaning up assets tarball from remote side...`); + execSync( + `kubectl --context="${context}" --namespace=languageforge exec -c app pod/${remotePodname} -- rm -f ${remoteTarball}`, + ); + } + } catch (_) {} + try { + if (localConn) await localConn.close(); + } catch (_) {} + try { + if (remoteConn) await remoteConn.close(); + } catch (_) {} + try { + if (portForwardProcess) await portForwardProcess.kill(); + } catch (_) {} +} + +async function randomFreePort() { + return new Promise((resolve) => { + const server = net.createServer(); + server.listen(0, () => { + // Asking for port 0 makes Node automatically find a free port + const port = server.address().port; + server.close((_) => resolve(port)); + }); + }); +} + +process.on("exit", cleanup); +process.on("uncaughtExceptionMonitor", cleanup); + +function run(cmd) { + return execSync(cmd).toString().trimEnd(); +} + +function getContexts() { + var stdout = run("kubectl config get-contexts -o name"); + return stdout.split("\n"); +} + +function reallyExists(name) { + // Sometimes the audio and/or pictures folders in assets are symlinks, and sometimes they're broken symlinks + // This returns true if the name is a real file/directory *or* a symlink with a valid target, or false if it doesn't exist or is broken + const result = execSync( + `kubectl --context=${context} --namespace=languageforge exec -c app pod/${remotePodname} -- sh -c "readlink -eq ${name} >/dev/null && echo yes || echo no"`, + ) + .toString() + .trimEnd(); + if (result === "yes") return true; + if (result === "no") return false; + throw new Error(`Unexpected result from readlink ${name}: ${result}`); +} + +// Sanity check + +var contexts = getContexts(); +if (!contexts.includes(stagingContext)) { + console.warn("Staging context not found. Tried", stagingContext, "but did not find it in", contexts); + console.warn("Might need to edit the top level of this file and try again"); + process.exit(1); +} +if (!contexts.includes(prodContext)) { + console.warn("Prod context not found. Tried", prodContext, "but did not find it in", contexts); + console.warn("Might need to edit the top level of this file and try again"); + process.exit(1); +} + +// Process args + +if (process.argv.length < 3) { + console.warn("Please pass project ID or URL as argument, e.g. node backup.mjs 5dbf805650b51914727e06c4"); + process.exit(2); +} + +let projId; +const arg = process.argv[2]; +if (URL.canParse(arg)) { + const url = new URL(arg); + if (url.pathname.startsWith("/app/lexicon/")) { + projId = url.pathname.substring("/app/lexicon/".length); + } else { + projId = url.pathname; // Will probably fail, but worth a try + } +} else { + projId = arg; +} + +let context = defaultContext; +let contextName = defaultContextName; + +if (process.argv.length > 3) { + const env = process.argv[3]; + switch (env) { + case "qa": + context = stagingContext; + contextName = "staging"; + break; + case "staging": + context = stagingContext; + contextName = "staging"; + break; + + case "live": + context = prodContext; + contextName = "production"; + break; + case "prod": + context = prodContext; + contextName = "production"; + break; + case "production": + context = prodContext; + contextName = "production"; + break; + + default: + console.warn(`Unknown environment ${env}`); + console.warn(`Valid values are qa, staging, live, prod, or production`); + process.exit(2); + } +} else { + console.warn("No environment selected. Defaulting to staging environment."); + console.warn('Pass "prod" or "production" as second arg to copy projects from production envrionment instead.'); +} + +projId = projId.trim(); + +console.warn(`Fetching project with ID ${projId} from ${contextName} context, named "${context}"`); +console.warn("If that looks wrong, hit Ctrl+C right NOW!"); +console.warn(); +console.warn("Pausing for 2 seconds to give you time to hit Ctrl+C..."); +await new Promise((resolve) => setTimeout(resolve, 2000)); +// Start running + +console.warn("Setting up kubectl port forwarding for remote Mongo..."); +const remoteMongoPort = await randomFreePort(); +let portForwardingReady; +const portForwardingPromise = new Promise((resolve) => { + portForwardingReady = resolve; +}); +portForwardProcess = spawn( + "kubectl", + [`--context=${context}`, "--namespace=languageforge", "port-forward", "deploy/db", `${remoteMongoPort}:27017`], + { + stdio: "pipe", + }, +); +portForwardProcess.stdout.on("data", (data) => { + portForwardingReady(); +}); +portForwardProcess.stderr.on("data", (data) => { + console.warn("Port forwarding failed:"); + console.warn(data.toString()); + console.warn("Exiting"); + process.exit(1); +}); + +console.warn("Setting up local Mongo connection..."); + +const localMongoPort = run("docker compose port db 27017").split(":")[1]; +const localConnStr = `mongodb://admin:pass@localhost:${localMongoPort}/?authSource=admin`; +localConn = await MongoClient.connect(localConnStr); + +const localAdmin = await localConn.db("scriptureforge").collection("users").findOne({ username: "admin" }); +const adminId = localAdmin._id.toString(); +console.log(`Local admin ID: ${adminId}`); +console.warn("If that doesn't look right, hit Ctrl+C NOW"); + +await portForwardingPromise; +console.warn("Port forwarding is ready. Setting up remote Mongo connection..."); + +const remoteConnStr = `mongodb://localhost:${remoteMongoPort}`; +remoteConn = await MongoClient.connect(remoteConnStr); + +console.warn("Remote Mongo connection established. Fetching project record..."); + +// Get project record +const project = await remoteConn + .db("scriptureforge") + .collection("projects") + .findOne({ _id: new ObjectId(projId) }); +console.log("Project code:", project.projectCode); + +const dbname = `sf_${project.projectCode}`; +project.users = { [adminId]: { role: "project_manager" } }; +project.ownerRef = new ObjectId(adminId); + +// Mongo removed the .copyDatabase method in version 4.2, whose release notes said to just use mongodump/mongorestore if you want to do that + +console.warn(`Copying ${dbname} database...`); +const collections = await remoteConn.db(dbname).collections(); +for (const remoteColl of collections) { + const name = remoteColl.collectionName; + console.log(` Copying ${name} collection...`); + const indexes = await remoteColl.indexes(); + const cursor = remoteColl.find(); + const docs = await cursor.toArray(); + const localColl = await localConn.db(dbname).collection(name); + try { + await localColl.drop(); + } catch (_) {} // Throws if collection doesn't exist, which is fine + try { + await localColl.dropIndexes(); + } catch (_) {} // Throws if collection doesn't exist, which is fine + if (indexes?.length) await localColl.createIndexes(indexes); + if (docs?.length) await localColl.insertMany(docs); + console.log(` ${docs.length} documents copied`); +} +console.warn(`${dbname} database successfully copied`); + +// Copy project record after its database has been copied, so there's never a race condition where the project exists but its entry database doesn't +console.warn("Copying project record..."); +await localConn + .db("scriptureforge") + .collection("projects") + .findOneAndReplace({ _id: new ObjectId(projId) }, project, { upsert: true }); + +// NOTE: mongodump/mongorestore approach below can be revived once Kubernetes 1.30 is installed on client *and* server, so kubectl exec is finally reliable + +// console.warn(`About to try fetching ${dbname} database from remote, will retry until success`); +// let done = false; +// while (!done) { +// try { +// console.warn(`Fetching ${dbname} database...`); +// execSync( +// `kubectl --context="${context}" --namespace=languageforge exec -i deploy/db -- mongodump --archive -d "${dbname}" > ${tempdir}/dump`, +// ); +// console.warn(`Uploading to local ${dbname} database...`); +// execSync(`docker exec -i lf-db mongorestore --archive --drop -d "${dbname}" ${localConnStr} < ${tempdir}/dump`); +// console.warn(`Successfully uploaded ${dbname} database`); +// done = true; +// } catch (err) { +// console.warn("mongodump failed, retrying..."); +// } +// } + +console.warn("Getting name of remote app pod..."); +remotePodname = run( + `kubectl --context="${context}" --namespace=languageforge get pod -o jsonpath="{.items[*]['metadata.name']}" -l app=app --field-selector "status.phase=Running"`, +); + +console.warn("Checking that remote assets really exist..."); +const includeAudio = reallyExists(`/var/www/html/assets/lexicon/${dbname}/audio`); +const includePictures = reallyExists(`/var/www/html/assets/lexicon/${dbname}/pictures`); +console.log(`Copy audio? ${includeAudio ? "yes" : "no"}`); +console.log(`Copy pictures? ${includePictures ? "yes" : "no"}`); + +const filesNeeded = []; +if (includeAudio) { + filesNeeded.push("audio"); +} +if (includePictures) { + filesNeeded.push("pictures"); +} + +if (filesNeeded.length === 0) { + console.warn("Project has no assets. Copy complete."); + process.exit(0); +} + +const tarTargets = filesNeeded.join(" "); + +console.warn("Creating assets tarball in remote..."); +remoteTarball = `/tmp/assets-${dbname}.tar`; +execSync( + `kubectl --context="${context}" --namespace=languageforge exec -c app pod/${remotePodname} -- tar chf ${remoteTarball} --owner=www-data --group=www-data -C "/var/www/html/assets/lexicon/${dbname}" ${tarTargets}`, +); +const sizeStr = run( + `kubectl --context="${context}" --namespace=languageforge exec -c app pod/${remotePodname} -- sh -c "ls -l ${remoteTarball} | cut -d' ' -f5"`, +); +const correctSize = +sizeStr; +console.warn(`Asserts tarball size is ${sizeStr}`); +console.warn("Trying to fetch assets tarball with kubectl cp..."); +let failed = false; +try { + execSync( + `kubectl --context="${context}" --namespace=languageforge cp ${remotePodname}:${remoteTarball} ${tempdir}/assets-${dbname}.tar`, + ); +} catch (_) { + console.warn("kubectl cp failed. Will try to continue with rsync..."); + failed = true; +} +if (!failed) { + const localSize = statSync(`${tempdir}/assets-${dbname}.tar`).size; + if (localSize < correctSize) { + console.warn(`Got only ${localSize} bytes instead of ${correctSize}. Will try to continue with rsync...`); + failed = true; + } +} +if (failed) { + console.warn("Ensuring rsync exists in target container..."); + execSync( + `kubectl exec --context="${context}" -c app pod/${remotePodname} -- bash -c "which rsync || (apt update && apt install rsync -y)"`, + ); + console.warn("\n===== IMPORTANT NOTE ====="); + console.warn( + "The rsync transfer may (probably will) stall at 100%. You'll have to find the rsync process and kill it. Sorry about that.", + ); + console.warn("===== IMPORTANT NOTE =====\n"); + let done = false; + while (!done) { + try { + execSync( + `rsync -v --partial --info=progress2 --rsync-path="/tmp/" --rsh="kubectl --context=${context} --namespace=languageforge exec -i -c app pod/${remotePodname} -- " "rsync:/tmp/assets-${dbname}.tar" "${tempdir}/"`, + { stdio: "inherit" }, // Allows us to see rsync progress + ); + done = true; + } catch (err) { + console.warn(`Rsync failed with error: ${err}. Retrying...`); + } + } +} +console.warn("Uploading assets tarball to local..."); +execSync( + `docker exec lf-app mkdir -p "/var/www/html/assets/lexicon/${dbname}" ; docker exec lf-app chown www-data:www-data "/var/www/html/assets/lexicon/${dbname}" || true`, +); +execSync(`docker cp - lf-app:/var/www/html/assets/lexicon/${dbname}/ < ${tempdir}/assets-${dbname}.tar`); +console.warn("Assets successfully uploaded"); + +process.exit(0); diff --git a/package.json b/package.json index 6bc9b0d7f2..497c84c99f 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "jasmine-spec-reporter": "^4.1.1", "lint-staged": "^13.0.3", "mini-css-extract-plugin": "^1.3.9", + "mongodb": "^6.6.2", "ng-annotate-loader": "^0.7.0", "ngtemplate-loader": "^2.1.0", "npm-run-all": "^4.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b69133e8ab..4efc797f41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,9 @@ importers: mini-css-extract-plugin: specifier: ^1.3.9 version: 1.6.2(webpack@5.91.0(webpack-cli@4.10.0)) + mongodb: + specifier: ^6.6.2 + version: 6.6.2 ng-annotate-loader: specifier: ^0.7.0 version: 0.7.0 @@ -827,6 +830,9 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@mongodb-js/saslprep@1.1.7': + resolution: {integrity: sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1009,9 +1015,15 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + '@types/webpack-env@1.18.4': resolution: {integrity: sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==} + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} @@ -1288,6 +1300,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bson@6.7.0: + resolution: {integrity: sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==} + engines: {node: '>=16.20.1'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2321,6 +2337,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -2395,6 +2414,36 @@ packages: engines: {node: '>=10'} hasBin: true + mongodb-connection-string-url@3.0.1: + resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} + + mongodb@6.6.2: + resolution: {integrity: sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -3014,6 +3063,9 @@ packages: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -3191,6 +3243,10 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + tryor@0.1.2: resolution: {integrity: sha512-2+ilNA00DGvbUYYbRrm3ux+snbo7I6uPXMw8I4p/QMl7HUOWBBZFbk+Mpr8/IAPDQE+LQ8vOdlI6xEzjc+e/BQ==} @@ -3342,6 +3398,10 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webm-fix-duration@1.0.1: resolution: {integrity: sha512-KaL2yq5BQ1ngRlWVpwQ0bI8INW3ZMzrbPfz8L50GvZHhReCeG5+zKqzk4BcpK6vEdMRHSxoNHn/ixiDqVdKZTw==} @@ -3421,6 +3481,10 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + whatwg-url@13.0.0: + resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} + engines: {node: '>=16'} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -4301,6 +4365,10 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@mongodb-js/saslprep@1.1.7': + dependencies: + sparse-bitfield: 3.0.3 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4510,8 +4578,14 @@ snapshots: '@types/trusted-types@2.0.7': {} + '@types/webidl-conversions@7.0.3': {} + '@types/webpack-env@1.18.4': {} + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/ws@8.5.10': dependencies: '@types/node': 17.0.45 @@ -4814,6 +4888,8 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) + bson@6.7.0: {} + buffer-from@1.1.2: {} builtin-modules@1.1.1: {} @@ -5877,6 +5953,8 @@ snapshots: dependencies: fs-monkey: 1.0.5 + memory-pager@1.5.0: {} + memorystream@0.3.1: {} merge-descriptors@1.0.1: {} @@ -5931,6 +6009,17 @@ snapshots: mkdirp@1.0.4: {} + mongodb-connection-string-url@3.0.1: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 13.0.0 + + mongodb@6.6.2: + dependencies: + '@mongodb-js/saslprep': 1.1.7 + bson: 6.7.0 + mongodb-connection-string-url: 3.0.1 + mrmime@2.0.0: {} ms@2.0.0: {} @@ -6561,6 +6650,10 @@ snapshots: sourcemap-codec@1.4.8: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -6748,6 +6841,10 @@ snapshots: dependencies: punycode: 2.3.1 + tr46@4.1.1: + dependencies: + punycode: 2.3.1 + tryor@0.1.2: {} ts-loader@9.5.1(typescript@5.4.5)(webpack@5.91.0(webpack-cli@4.10.0)): @@ -6907,6 +7004,8 @@ snapshots: webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: {} + webm-fix-duration@1.0.1: {} webpack-bundle-analyzer@4.10.2: @@ -7054,6 +7153,11 @@ snapshots: websocket-extensions@0.1.4: {} + whatwg-url@13.0.0: + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0