diff --git a/.vscode/launch.json b/.vscode/launch.json index f2df090b26..5a117a8249 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,6 +14,15 @@ "console": "integratedTerminal", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["${fileBasenameNoExtension}"] + }, + { + "name": "Debug functions-v2 test", + "request": "launch", + "type": "node", + "console": "integratedTerminal", + "program": "${workspaceFolder}/functions-v2/node_modules/.bin/jest", + "args": ["${fileBasenameNoExtension}"], + "cwd": "${workspaceFolder}/functions-v2" } ] } diff --git a/README.md b/README.md index 40174c2a08..608a1ffc13 100644 --- a/README.md +++ b/README.md @@ -59,109 +59,49 @@ To deploy a production release: ## Developing/deploying cloud functions -CLUE uses several Google cloud functions to implement certain features that would be difficult (or impossible) to implement entirely client-side. -|Function|Purpose| -|--------|-------| -|_getImageData_|Retrieves image data that may reside in other classes and hence is not accessible client-side, e.g. for supports published to multiple classes or documents retrieved via the teacher network.| -|_getNetworkDocument_|Retrieves the contents of a document accessible to a teacher via the teacher network.| -|_getNetworkResources_|Retrieves the list of resources (documents) available to a teacher via the teacher network.| -|_postDocumentComment_|Posts a comment to a document in firestore, adding metadata for the document to firestore if necessary.| -|_publishSupport_|Publishes a document as a support that is accessible to all of a teacher's classes (including any referenced images).| -|_validateCommentableDocument_|Checks whether a specific commentable document exists in firestore and creates it if necessary.| - -The code for the functions is in the `functions` directory. You should be able to cd into the -`functions` directory and perform basic development operations: -``` -$ cd functions -$ npm install # install local dependencies -$ npm run lint # lint the functions code -$ npm run test # runs jest (unit) tests for the functions code -$ npm run build # build the functions code (transpile TypeScript) -``` -### Note 1 -There seems to be an uneasy relationship between the `node_modules` folder in the -`functions` directory and the one in the parent directory. I had to explicitly specify the -path to typescript in the `build` function. There's probably a better configuration available, -but in the meantime this seems to mostly work. - -### Note 2 -When running `npm run test` with node 16, the following error is shown -``` -TypeError: Cannot read properties of undefined (reading 'INTERNAL') -``` -This error is triggered by the following line in `test-utils.ts` -``` -import { useEmulators } from "@firebase/rules-unit-testing"; -``` -The current work around is to use node 14 to run the tests. +CLUE uses several Google cloud functions to implement certain features that would be difficult (or impossible) to implement entirely client-side. There are two folders of functions `functions-v1` and `functions-v2`. We are trying to incrementally migrate the v1 functions into the v2 folder. -See functions/dependency-notes.md for more on this. +Each folder has its own readme: +- [functions-v2](functions-v2/README.md) +- [functions-v1](functions-v1/README.md) -### Testing cloud functions - -Google recommends (requires?) that [firebase-tools](https://www.npmjs.com/package/firebase-tools) be installed globally: -``` -$ npm install -g firebase-tools -``` -This should be run periodically to make sure you're running the latest version of the tools. +## Testing/Deploying database rules -#### Running tests locally (without running functions in the emulator) -``` -$ npm run serve # build and then start the emulators -$ npm run test # run all tests in `functions` directory -$ npm run test -- some.test.ts # run a particular test -``` -The existing tests currently work this way. They test the basic functionality of the cloud functions by importing and calling them directly from node.js test code. This is a simple and efficient way of testing the basic functionality without all the overhead of the functions emulator. The downside is that the node.js test environment is not the same as the hosted function environment. For instance, it's possible to return objects in node.js that can't be JSON-stringified which will throw an error when the function is hosted. That said, you can't beat the convenience of simply calling the functions directly. +### Requirements -#### Running local tests against functions hosted in the emulator -To run jest tests against functions running in the emulator requires [serving functions using a Cloud Functions Shell](https://firebase.google.com/docs/functions/local-shell#serve_functions_using_a_cloud_functions_shell). Currently, all of our functions are `HTTPS Callable` functions, which [can be called](https://firebase.google.com/docs/functions/local-shell#invoke_https_callable_functions) in this shell mode, but: ->Emulation of context.auth is currently unavailable. +- The tests currently only run with Node.js version 16.x +- You need the firebase CLI. Version 12 is compatible with Node 16: `npm install -g firebase-tools@12` +- You should be logged in to firebase: `firebase login` -#### Running CLUE against functions running locally in the emulator: -``` -$ npm run serve # build and then start the functions emulator -``` -and launch CLUE with url parameter `functions=emulator`. +Java is also required for running the emulators. There are various ways to install it; I did this: -### To deploy firebase functions to production: -``` -$ npm run deploy # deploy all functions -$ npm run deploy:getImageData # deploy individual function -$ npm run deploy:postDocumentComment # deploy individual function +```shell +brew install java +echo 'export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"' >> ~/.zshrc ``` -By convention, our firebase functions have an internal version number that is returned with any results. This should be incremented appropriately when new versions are deployed. This will allow us to determine whether the current code in GitHub has been deployed or not, for instance. Also by convention, our firebase functions accept parameters of `{ warmUp: true }` which can be issued in advance of any actual call to mitigate the google cloud function cold-start issue. - -### Serving CLUE from https://localhost -To test the deployed function(s) from your local development environment, you may need to run your local dev server with https to avoid CORS errors. To do so, [create a certificate](https://www.matthewhoelter.com/2019/10/21/how-to-setup-https-on-your-local-development-environment-localhost-in-minutes.html) in your `~/.localhost-ssl` directory and name the files `localhost.pem` and `localhost.key`. To use the certificate: -``` -$ npm run start:secure -``` - -## Testing/Deploying database rules - -### Requirements: - - * You should install the firebase CLI via: `npm install -g firebase-tools` - * You should be logged in to firebase: `firebase login` - Firestore security rules are unit tested and realtime database rules could be with some additional work. ### To test database rules -``` -$ cd firebase-test -$ npm run test + +The emulator must be running when the test is invoked. + +```shell +cd firebase-test +npm run start & +npm run test ``` +### To deploy database rules + You deploy firebase functions and rules directly from the working directory using the `firebase deploy` command. You can see `firebase deploy help` for more info. See which project you have access to and which you are currently using via: `firebase projects:list` -### To deploy database rules: -``` -$ npm run deploy:firestore:rules # deploys firestore rules -$ npm run deploy:firebase:rules # deploys firebase (realtime database) rules +```shell +npm run deploy:firestore:rules # deploys firestore rules +npm run deploy:firebase:rules # deploys firebase (realtime database) rules ``` ## Debugging @@ -174,6 +114,7 @@ To enable per component debugging set the "debug" localstorage key with one or m - `docList` - this will print a table of information about a list of documents - `document` this will add the active document as `window.currentDocument`, you can use MST's hidden toJSON() like `currentDocument.toJSON()` to views its content. - `drop` console log the dataTransfer object from drop events on the document. +- `firestore` turn on Firestore's internal debugging, this logs all queries to Firestore. - `history` this will: print some info to the console as the history system records changes, print the full history as JSON each time it is loaded from Firestore, and provide a `window.historyDocument` so you can inspect the document while navigating the history. - `images` this will set `window.imageMap` so you can look at the status and URLs of images that have been loaded. - `listeners` console log the adding, removing, and firing of firebase listeners @@ -214,7 +155,6 @@ There are a number of URL parameters that can aid in testing: |`fakeClass` |string |Class id for demo, qa, or test modes.| |`fakeUser` |`(student\|teacher):`|Configure user type and (optionally) id.| |`qaGroup` |string |Group id for qa, e.g. automated tests.| -|`qaClear` |`all\|class\|offering` |Extent of database clearing for automated tests.| |`firebase` |`emulator\|` |Target emulator for firebase realtime database calls.| |`firestore` |`emulator\|` |Target emulator for firestore database calls.| |`functions` |`emulator\|` |Target emulator-hosted firebase functions.| @@ -242,11 +182,11 @@ The Standalone Document Editor also supports a `readOnly` url param. If you spec ### QA -Along with `dev`, `test`, `authed` and `demo` modes the app has a `qa` mode. QA mode uses the same parameters as demo mode with two additional parameters: +Along with `dev`, `test`, `authed` and `demo` modes the app has a `qa` mode. QA mode uses the same parameters as demo mode with one additional parameter: + +qaGroup - the group to automatically assign the fake user to after connecting to the database. -1. qaGroup - the group to automatically assign the fake user to after connecting to the database. -2. qaClear - either "all", "class" or "offering". When this parameter is present the QA database is cleared at the level requested based on the user parameters. - This is useful to clear data between automated QA runs. When complete the app will display `QA Cleared: OK`. +Additionally in `qa` mode the "root" in Firestore and the Realtime database is based on the Firebase user uid. This user is stored in session storage so each new tab will start a new root. In Cypress session storage is cleared between tests so each new test will have its own root. ### To run Cypress integration tests: - `npm run test:local` @@ -268,7 +208,6 @@ implementation simpler. Existing commands at the moment: - setupGroup - upLoadFile - - clearQAData ## License diff --git a/cypress.config.ts b/cypress.config.ts index a6ef422760..0df516afeb 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -35,6 +35,7 @@ export default defineConfig({ qaMothPlotUnitStudent5: "/?appMode=qa&fakeClass=5&fakeUser=student:5&qaGroup=5&problem=1.1&unit=./demo/units/qa-moth-plot/content.json", qaNoSectionProblemTabUnitStudent5: "/?appMode=qa&fakeClass=5&fakeUser=student:5&qaGroup=5&problem=1.1&unit=./demo/units/qa-no-section-problem-tab/content.json", clueTestqaUnitStudent5: "/?appMode=demo&demoName=CLUE-Test&fakeClass=5&fakeUser=student:5&problem=1.1&unit=./demo/units/qa/content.json&noPersistentUI", + clueTestNoUnitStudent5: "/?appMode=demo&demoName=CLUE-Test&fakeClass=5&fakeUser=student:5&problem=1.1&noPersistentUI", clueTestqaUnitTeacher6: "/?appMode=demo&demoName=CLUE-Test&fakeClass=5&fakeUser=teacher:6&problem=1.1&unit=./demo/units/qa/content.json&noPersistentUI", clueTestqaConfigSubtabsUnitTeacher6: "/?appMode=demo&demoName=CLUE-Test&fakeClass=5&fakeUser=teacher:6&problem=1.1&unit=qa-config-subtabs&noPersistentUI", e2e: { diff --git a/cypress/e2e/cleanup/remove_teacher_comment_spec.js b/cypress/e2e/cleanup/remove_teacher_comment_spec.js index 376d81d925..df7970ca66 100644 --- a/cypress/e2e/cleanup/remove_teacher_comment_spec.js +++ b/cypress/e2e/cleanup/remove_teacher_comment_spec.js @@ -56,7 +56,6 @@ function beforePortalTest(url, clueTeacher, reportUrl) { function beforeTest() { const queryParams = `${Cypress.config("qaUnitTeacher6Network")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.openTopTab("problems"); @@ -78,7 +77,7 @@ describe('Delete Teacher Comments In chat panel', () => { // Teacher 1 tile comment chatPanel.deleteTeacherComments(); }); - + cy.log("login teacher2 and setup clue chat"); cy.logout(portalUrl); beforePortalTest(portalUrl, clueTeacher2, reportUrl2); @@ -93,7 +92,7 @@ describe('Delete Teacher Comments In chat panel', () => { cy.clickProblemResourceTile(tab.sectionCode); // Teacher 2 tile comment chatPanel.deleteTeacherComments(); - }); + }); }); it('Delete chat panel comment tags', () => { beforeTest(); @@ -102,7 +101,7 @@ describe('Delete Teacher Comments In chat panel', () => { cy.openTopTab("problems"); cy.openProblemSection("Introduction"); chatPanel.deleteTeacherComments(); - + cy.log('Delete comment tags on tile comment'); cy.openTopTab("problems"); cy.clickProblemResourceTile('introduction'); diff --git a/cypress/e2e/functional/document_tests/bookmark_test_spec.js b/cypress/e2e/functional/document_tests/bookmark_test_spec.js index a59fa14f69..dc5d503eb2 100644 --- a/cypress/e2e/functional/document_tests/bookmark_test_spec.js +++ b/cypress/e2e/functional/document_tests/bookmark_test_spec.js @@ -12,7 +12,6 @@ const queryParams1 = `${Cypress.config("qaConfigSubtabsUnitStudent5")}`; const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } @@ -76,7 +75,7 @@ context('Bookmarks', function () { // resourcesPanel.getCanvasStarIcon('class-work', 'workspaces', copyDocumentTitle).should('have.class', 'starred'); // cy.openSection('class-work', 'bookmarks'); // resourcesPanel.getCanvasItemTitle('class-work', 'bookmarks').contains(copyDocumentTitle).should('exist'); - }) + }); it('Test bookmarks for teacher', function () { beforeTest(queryParams1); let copyDocumentTitle = 'copy Investigation'; @@ -120,5 +119,5 @@ context('Bookmarks', function () { resourcesPanel.getCanvasStarIcon('class-work', 'workspaces', title).should('have.class', 'starred'); cy.openSection('class-work', 'bookmarks'); resourcesPanel.getCanvasItemTitle('class-work', 'bookmarks').contains(title).should('exist'); - }) -}) + }); +}); diff --git a/cypress/e2e/functional/document_tests/canvas_test_spec.js b/cypress/e2e/functional/document_tests/canvas_test_spec.js index d087b743ed..3d890665d2 100644 --- a/cypress/e2e/functional/document_tests/canvas_test_spec.js +++ b/cypress/e2e/functional/document_tests/canvas_test_spec.js @@ -28,7 +28,6 @@ const queryParams4 = "?appMode=demo&demoName=BrokenDocs&fakeClass=1&fakeUser=stu const title = "QA 1.1 Solving a Mystery with Proportional Reasoning"; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/document_tests/copy_doc_test_spec.js b/cypress/e2e/functional/document_tests/copy_doc_test_spec.js index 244beeddb4..1d8dc319de 100644 --- a/cypress/e2e/functional/document_tests/copy_doc_test_spec.js +++ b/cypress/e2e/functional/document_tests/copy_doc_test_spec.js @@ -31,7 +31,6 @@ let canvas = new Canvas; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/document_tests/exemplar_test_spec.js b/cypress/e2e/functional/document_tests/exemplar_test_spec.js index d5b7296839..8f0905498b 100644 --- a/cypress/e2e/functional/document_tests/exemplar_test_spec.js +++ b/cypress/e2e/functional/document_tests/exemplar_test_spec.js @@ -18,7 +18,6 @@ const exemplarName = "First Exemplar"; const exemplarInfo = "Ivan Idea: First Exemplar"; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } @@ -53,6 +52,41 @@ context('Exemplar Documents', function () { clueCanvas.getStickyNotePopup().should("not.exist"); }); + it('Exemplars show up in the correct place in the sort work view', function () { + beforeTest(queryParams2); + cy.openTopTab('sort-work'); + + // With no secondary sort, the full exemplar tile should show up in the right sections. + sortWork.openSortWorkSection("No Group"); + sortWork.checkDocumentInGroup("No Group", exemplarName); + + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByNameOption().click(); + sortWork.openSortWorkSection("Idea, Ivan"); + sortWork.checkDocumentInGroup("Idea, Ivan", exemplarName); + + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByTagOption().click(); + sortWork.openSortWorkSection("Varies Material/Surface"); + sortWork.checkDocumentInGroup("Varies Material/Surface", exemplarName); + + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByBookmarkedOption().click(); + sortWork.openSortWorkSection("Not Bookmarked"); + sortWork.checkDocumentInGroup("Not Bookmarked", exemplarName); + + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByToolsOption().click(); + sortWork.openSortWorkSection("Text"); + sortWork.checkDocumentInGroup("Text", exemplarName); + + // With a secondary sort, "simple documents" (little boxes) should show up for exemplars. + + sortWork.getSecondarySortByMenu().click(); + sortWork.getSecondarySortByNameOption().click(); + sortWork.checkSimpleDocumentInSubgroup("Text", "Idea, Ivan", exemplarInfo); + }); + it('Unit with exemplars hidden initially, revealed 3 drawings and 3 text tiles', function () { beforeTest(queryParams1); cy.openTopTab('sort-work'); diff --git a/cypress/e2e/functional/document_tests/group_chooser_spec.js b/cypress/e2e/functional/document_tests/group_chooser_spec.js index 4c1895dd09..af0eee792c 100644 --- a/cypress/e2e/functional/document_tests/group_chooser_spec.js +++ b/cypress/e2e/functional/document_tests/group_chooser_spec.js @@ -20,10 +20,6 @@ const defaultSetupOptions = { problem }; -function beforeTest() { - cy.clearQAData('all'); -} - function setup(student, opts = {}) { const options = { ...defaultSetupOptions, ...opts }; cy.visit('/?appMode=qa&fakeClass=' + fakeClass + '&fakeUser=student:' + student + '&problem=' + options.problem + '&unit=./demo/units/qa/content.json'); @@ -40,8 +36,6 @@ function setup(student, opts = {}) { context('Test student join a group', function () { it('Test student join a group', function () { - beforeTest(); - cy.log('Student 1 will join and will verify Join Group Dialog comes up with welcome message to correct student'); setup(student1); cy.get('.app > .join > .join-title').should('contain', 'Join Group'); diff --git a/cypress/e2e/functional/document_tests/group_test_readonly_spec.js b/cypress/e2e/functional/document_tests/group_test_readonly_spec.js index e312142c03..f6f3548f6d 100644 --- a/cypress/e2e/functional/document_tests/group_test_readonly_spec.js +++ b/cypress/e2e/functional/document_tests/group_test_readonly_spec.js @@ -25,7 +25,6 @@ context('Test group functionalities', function () { it('4-up view read-only', function () { cy.log('students to check each others tiles in 4-up view read-only'); - cy.clearQAData('all'); setupTest(0); setupTest(1); diff --git a/cypress/e2e/functional/document_tests/group_test_spec.js b/cypress/e2e/functional/document_tests/group_test_spec.js index 6d87fdbcf4..273721a135 100644 --- a/cypress/e2e/functional/document_tests/group_test_spec.js +++ b/cypress/e2e/functional/document_tests/group_test_spec.js @@ -45,7 +45,6 @@ context('Test group functionalities', function () { it('4-up view tests', function () { cy.log('will set up groups'); - cy.clearQAData('all'); setupTest(0); setupTest(1); setupTest(2); diff --git a/cypress/e2e/functional/document_tests/header_test_spec.js b/cypress/e2e/functional/document_tests/header_test_spec.js index 1f5832bf39..2805f4703a 100644 --- a/cypress/e2e/functional/document_tests/header_test_spec.js +++ b/cypress/e2e/functional/document_tests/header_test_spec.js @@ -4,7 +4,6 @@ const header = new Header; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/document_tests/nav_panel_test_spec.js b/cypress/e2e/functional/document_tests/nav_panel_test_spec.js index 58d3da527b..2084cbfcfa 100644 --- a/cypress/e2e/functional/document_tests/nav_panel_test_spec.js +++ b/cypress/e2e/functional/document_tests/nav_panel_test_spec.js @@ -17,7 +17,6 @@ const queryParams4 = `${Cypress.config("qaNoSectionProblemTabUnitStudent5")}`; const queryParams5 = `${Cypress.config("qaConfigSubtabsUnitStudent5")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/document_tests/student_teacher_4up_readonly_spec.js b/cypress/e2e/functional/document_tests/student_teacher_4up_readonly_spec.js index add46e38f4..b13e982803 100644 --- a/cypress/e2e/functional/document_tests/student_teacher_4up_readonly_spec.js +++ b/cypress/e2e/functional/document_tests/student_teacher_4up_readonly_spec.js @@ -144,8 +144,6 @@ function setupTestBrain(studentIndex) { context('Test 4-up and 1-up views tiles read only functionalities', function () { it('4-up and 1-up views read-only text, table, geometry, drawing, expression, numberline, image, datacard tiles', function () { - cy.clearQAData('all'); - setupTest(0); setupTest(1); @@ -174,8 +172,6 @@ context('Test 4-up and 1-up views tiles read only functionalities', function () }); it('4-up and 1-up views read-only dataflow, expression, xy plot tiles', function () { - cy.clearQAData('all'); - setupTestBrain(0); setupTestBrain(1); diff --git a/cypress/e2e/functional/document_tests/student_test_spec.js b/cypress/e2e/functional/document_tests/student_test_spec.js index c866335045..e600f687f9 100644 --- a/cypress/e2e/functional/document_tests/student_test_spec.js +++ b/cypress/e2e/functional/document_tests/student_test_spec.js @@ -1,24 +1,24 @@ import Header from '../../../support/elements/common/Header'; import ClueHeader from '../../../support/elements/common/cHeader'; +import SortedWork from "../../../support/elements/common/SortedWork"; const header = new Header; const clueHeader = new ClueHeader; +const sortWork = new SortedWork; let student = '5', classroom = '5', group = '5'; -function beforeTest() { - const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); +function beforeTest(queryParams) { cy.visit(queryParams); cy.waitForLoad(); } context('Check header area for correctness', function () { it('verify header area', function () { - beforeTest(); + beforeTest(`${Cypress.config("qaUnitStudent5")}`); cy.log('will verify if class name is correct'); header.getClassName().should('contain', 'Class ' + classroom); @@ -43,3 +43,25 @@ context('Check header area for correctness', function () { }); }); +context("check public/private document access", function() { + it("marks private documents as private and only shows public documents as accessible", function() { + const queryParams = (`${Cypress.config("clueTestNoUnitStudent5")}`); + beforeTest(queryParams); + + cy.openTopTab("sort-work"); + cy.get(".section-header-arrow").click({multiple: true}); // Open all sections + cy.log("will verify if private documents are marked as private and are not accessible"); + sortWork.checkGroupDocumentVisibility("No Group", true, true); + cy.log("will verify if user's own documents are not marked as private and are accessible"); + sortWork.checkGroupDocumentVisibility("Group 2", false, true); + + // Check the above for a view that contains compact document items + sortWork.getShowForMenu().click(); + sortWork.getShowForInvestigationOption().click(); + cy.get(".section-header-arrow").click({multiple: true}); // Open all sections + cy.log("will verify if private documents are marked as private and are not accessible in the compact view"); + sortWork.checkGroupDocumentVisibility("No Group", true); + cy.log("will verify if user's own documents are not marked as private and are accessible in the compact view"); + sortWork.checkGroupDocumentVisibility("Group 2", false); + }); +}); diff --git a/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js b/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js index 58207dbdf5..30bf1269f5 100644 --- a/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js +++ b/cypress/e2e/functional/document_tests/tiles_copy_test_spec.js @@ -54,7 +54,6 @@ const tiles2 = [ ]; function beforeTest(queryParams) { - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/document_tests/workspace_test_spec.js b/cypress/e2e/functional/document_tests/workspace_test_spec.js index 48d62d6b10..00cf920104 100644 --- a/cypress/e2e/functional/document_tests/workspace_test_spec.js +++ b/cypress/e2e/functional/document_tests/workspace_test_spec.js @@ -6,7 +6,6 @@ let clueCanvas = new ClueCanvas, function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/teacher_tests/history_playback_spec.js b/cypress/e2e/functional/teacher_tests/history_playback_spec.js index 141011d9e3..bd32d99cdc 100644 --- a/cypress/e2e/functional/teacher_tests/history_playback_spec.js +++ b/cypress/e2e/functional/teacher_tests/history_playback_spec.js @@ -17,7 +17,6 @@ function moveSliderTo(percent) { } function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); clueCanvas.getInvestigationCanvasTitle().text().then((investigationTitle) => { diff --git a/cypress/e2e/functional/teacher_tests/teacher_chat_spec.js b/cypress/e2e/functional/teacher_tests/teacher_chat_spec.js index ef92289a7d..43ebd7c8a2 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_chat_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_chat_spec.js @@ -34,7 +34,6 @@ const ss = [{ "section": "problems", const comment = [ "This is document comment for ", "This is tile comment for " ]; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); cy.openTopTab("problems"); @@ -44,7 +43,6 @@ function beforeTest(params) { } function beforeTestCommentedDocumentList(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); cy.openTopTab("my-work"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_curation_spec.js b/cypress/e2e/functional/teacher_tests/teacher_curation_spec.js index 91b4191038..169d047392 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_curation_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_curation_spec.js @@ -12,7 +12,6 @@ const defaultProblemDocTitle = "QA 1.1 Solving a Mystery with Proportional Reaso function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_dashboard_4_quadrants_spec.js b/cypress/e2e/functional/teacher_tests/teacher_dashboard_4_quadrants_spec.js index 3b76e2aef3..ce4b3cdbc8 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_dashboard_4_quadrants_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_dashboard_4_quadrants_spec.js @@ -4,7 +4,6 @@ let dashboard = new TeacherDashboard(); function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Dashboard"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_dashboard_spec.js b/cypress/e2e/functional/teacher_tests/teacher_dashboard_spec.js index 21087d44f1..97a8f90d2f 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_dashboard_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_dashboard_spec.js @@ -6,7 +6,6 @@ let clueCanvas = new ClueCanvas; function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Dashboard"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_network_spec.js b/cypress/e2e/functional/teacher_tests/teacher_network_spec.js index 43df7a03f3..a580b7c2b9 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_network_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_network_spec.js @@ -8,7 +8,6 @@ let teacherNetwork = new TeacherNetwork; function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js index 25184ddfe8..1060f74a63 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js @@ -8,7 +8,6 @@ const teacherQueryParams = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`; const studentQueryParams = `${Cypress.config("qaConfigSubtabsUnitStudent5")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); @@ -23,46 +22,43 @@ function verifySwitch(publicOrPrivate) { function verifyStudentSeesAsPrivate() { cy.get('.tab-sort-work').click(); cy.get('.section-header-arrow').click({multiple: true}); - cy.get('.thumbnail-private').should('exist'); + cy.contains('[data-test="sort-work-list-items"]','Teacher 1:') + .should('have.descendants', '.thumbnail-private'); } function verifyStudentSeesAsPublic() { cy.get('.tab-sort-work').click(); cy.get('.section-header-arrow').click({multiple: true}); - cy.get('.thumbnail-public').should('not.exist'); + cy.contains('[data-test="sort-work-list-items"]','Teacher 1:') + .should('not.have.descendants', '.thumbnail-private'); } context('Teacher Sharing', function() { - describe('verify share functionality', function() { - it('loads teacher document as private', function() { - beforeTest(teacherQueryParams); - verifySwitch('private'); - }); + it('verify share functionality', function() { + cy.log('loads teacher document as private'); + beforeTest(teacherQueryParams); + verifySwitch('private'); - it('does not allow student to access private teacher document', function() { - cy.visit(studentQueryParams); - cy.waitForLoad(); - verifyStudentSeesAsPrivate(); - }); + cy.log('does not allow student to access private teacher document'); + cy.visit(studentQueryParams); + cy.waitForLoad(); + verifyStudentSeesAsPrivate(); - it('allows teacher to share a document', function() { - cy.visit(teacherQueryParams); - cy.waitForLoad(); - clueCanvas.shareCanvas(); - verifySwitch('public'); - }); + cy.log('allows teacher to share a document'); + cy.visit(teacherQueryParams); + cy.waitForLoad(); + clueCanvas.shareCanvas(); + verifySwitch('public'); - it('allows student to access public teacher document', function() { - cy.visit(studentQueryParams); - cy.waitForLoad(); - verifyStudentSeesAsPublic(); - }); + cy.log('allows student to access public teacher document'); + cy.visit(studentQueryParams); + cy.waitForLoad(); + verifyStudentSeesAsPublic(); - it('allows teacher to unshare a document', function() { - cy.visit(teacherQueryParams); - cy.waitForLoad(); - clueCanvas.unshareCanvas(); - verifySwitch('private'); - }); + cy.log('allows teacher to unshare a document'); + cy.visit(teacherQueryParams); + cy.waitForLoad(); + clueCanvas.unshareCanvas(); + verifySwitch('private'); }); }); diff --git a/cypress/e2e/functional/teacher_tests/teacher_six_pack_spec.js b/cypress/e2e/functional/teacher_tests/teacher_six_pack_spec.js index d12482c572..3c4db63d23 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_six_pack_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_six_pack_spec.js @@ -4,7 +4,6 @@ let dashboard = new TeacherDashboard(); function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Dashboard"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_sort_work_view_spec.js b/cypress/e2e/functional/teacher_tests/teacher_sort_work_view_spec.js index 1da2d6ff5a..f2c6146c2f 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_sort_work_view_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_sort_work_view_spec.js @@ -18,7 +18,6 @@ const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`; const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); @@ -45,16 +44,16 @@ describe('SortWorkView Tests', () => { it('should open SortWorkView tab and interact with it', () => { beforeTest(queryParams1); cy.log('verify clicking the sort menu'); - sortWork.getSortByMenu().click(); // Open the sort menu + sortWork.getPrimarySortByMenu().click(); // Open the sort menu cy.wait(1000); - sortWork.getSortByNameOption().click(); //Select 'Name' sort type + sortWork.getPrimarySortByNameOption().click(); //Select 'Name' sort type cy.wait(1000); - sortWork.getSortByMenu().click(); // Open the sort menu again + sortWork.getPrimarySortByMenu().click(); // Open the sort menu again cy.wait(1000); - sortWork.getSortByGroupOption().click(); // Select 'Group' sort type + sortWork.getPrimarySortByGroupOption().click(); // Select 'Group' sort type cy.wait(1000); cy.log('verify opening and closing a document from the sort work view'); @@ -66,10 +65,118 @@ describe('SortWorkView Tests', () => { sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed }); + it("should open Sort Work tab and test showing by Problem, Investigation, Unit, All", () => { + beforeTest(queryParams1); + + sortWork.getShowForMenu().should("be.visible"); + sortWork.getShowForProblemOption().should("have.class", "selected"); // "Problem" selected by default + sortWork.getShowForInvestigationOption().should("exist"); + sortWork.getShowForUnitOption().should("exist"); + sortWork.getShowForAllOption().should("exist"); + + cy.get(".section-header-arrow").click({multiple: true}); // Open the sections + // For the "Problem" option, documents should be listed using the larger thumbnail view + cy.get("[data-test=sort-work-list-items]").should("have.length.greaterThan", 0); + cy.get("[data-test=simple-document-item]").should("not.exist"); + sortWork.getShowForMenu().click(); + cy.wait(500); + sortWork.getShowForInvestigationOption().click(); + cy.wait(500); + // For the "Investigation", "Unit", and "All" options, documents should be listed using the smaller "simple" view + cy.get("[data-test=sort-work-list-items]").should("not.exist"); + cy.get("[data-test=simple-document-item]").should("have.length.greaterThan", 0); + sortWork.getShowForMenu().click(); + cy.wait(500); + sortWork.getShowForUnitOption().click(); + cy.wait(500); + cy.get("[data-test=sort-work-list-items]").should("not.exist"); + cy.get("[data-test=simple-document-item]").should("have.length.greaterThan", 0); + sortWork.getShowForMenu().click(); + cy.wait(500); + sortWork.getShowForAllOption().click(); + cy.wait(500); + cy.get("[data-test=sort-work-list-items]").should("not.exist"); + cy.get("[data-test=simple-document-item]").should("have.length.greaterThan", 0); + cy.get("[data-test=simple-document-item]").should("have.attr", "title").and("not.be.empty"); + cy.get("[data-test=simple-document-item]").first().click(); + sortWork.getFocusDocument().should("be.visible"); + }); + + it("should open Sort Work tab and test secondary sort functionality", () => { + beforeTest(queryParams1); + + cy.get(".section-header-arrow").click({multiple: true}); // Open the sections + cy.get("[data-testid=section-sub-header]").should("not.exist"); + cy.get("[data-testid=doc-group]").should("not.exist"); + cy.get("[data-testid=doc-group-label]").should("not.exist"); + cy.get("[data-testid=doc-group-list]").should("not.exist"); + + // Switching from "Show for" from Problem to Investigation should switch the list of + // documents from the larger thumbnail view to the smaller "simple" view and arrange the + // document list items in rows that are potentially scrollable. + sortWork.getShowForMenu().click(); + sortWork.getShowForInvestigationOption().click(); + cy.get("[data-testid=section-sub-header]").should("not.exist"); + cy.get("[data-testid=doc-group]").should("exist"); + // There should be one doc group per section-document-list. There is no + // label for the doc group. + cy.get("[data-testid=section-document-list]").each($el => { + cy.wrap($el).find("[data-testid=doc-group]").should("have.length", 1); + cy.wrap($el).find("[data-testid=doc-group-label]").should("not.exist"); + }); + cy.get("[data-testid=doc-group-list]").invoke("prop", "scrollLeft").should("be.eq", 0); + cy.get("[data-testid=scroll-button-left]").should("exist").and("be.disabled"); + cy.get("[data-testid=scroll-button-right]").should("exist").and("not.be.disabled"); + cy.get("[data-testid=scroll-button-right]").click(); + cy.get("[data-testid=scroll-button-left]").should("exist").and("not.be.disabled"); + cy.get("[data-testid=doc-group-list]").invoke("prop", "scrollLeft").should("be.gt", 0); + cy.get("[data-testid=scroll-button-left]").click(); + cy.get("[data-testid=scroll-button-left]").should("exist").and("be.disabled"); + cy.get("[data-testid=doc-group-list]").invoke("prop", "scrollLeft").should("be.eq", 0); + + // Apply secondary sort + sortWork.getSecondarySortByMenu().click(); + sortWork.getSecondarySortByNoneOption().should("have.class", "selected"); + sortWork.getSecondarySortByGroupOption().should("exist"); + sortWork.getSecondarySortByTagOption().should("exist"); + sortWork.getSecondarySortByBookmarkedOption().should("exist"); + sortWork.getSecondarySortByToolsOption().should("exist"); + sortWork.getSecondarySortByNameOption().should("exist").click(); + cy.wait(500); + + sortWork.getSecondarySortByNoneOption().should("not.have.class", "selected"); + sortWork.getSecondarySortByNameOption().should("have.class", "selected"); + cy.get("[data-testid=section-sub-header]").each($el => { + cy.wrap($el).should("exist").and("have.text", "Name"); + }); + cy.get("[data-testid=doc-group]").should("exist"); + // There should be multiple doc groups that are children of each section-document-list. + // Each doc group should have its own label. + cy.get("[data-testid=section-document-list]").each($el => { + cy.wrap($el).find("[data-testid=doc-group]").should("have.length.be.greaterThan", 1).each($group => { + cy.wrap($group).find("[data-testid=doc-group-label]").should("have.length", 1); + }); + }); + + // Change the primary sort option to match the currently-selected secondary sort option, and + // make sure the latter automatically resets to "None", and the previously-selected option in + // the primary menu is now selectable in the secondary sort menu. + sortWork.getPrimarySortByGroupOption().should("have.class", "selected"); + sortWork.getSecondarySortByGroupOption().should("have.class", "disabled"); + sortWork.getSecondarySortByNameOption().should("have.class", "selected"); + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByNameOption().click(); + cy.wait(500); + sortWork.getPrimarySortByGroupOption().should("not.have.class", "selected"); + sortWork.getPrimarySortByNameOption().should("have.class", "selected"); + sortWork.getSecondarySortByGroupOption().should("have.class", "enabled"); + sortWork.getSecondarySortByNameOption().should("not.have.class", "selected").and("have.class", "disabled"); + sortWork.getSecondarySortByNoneOption().should("have.class", "selected"); + + }); - it("should open Sort Work tab and test sorting by group", () => { - // Clear data before the test so it can be retried and will start with a clean slate - cy.clearQAData('all'); + // TODO: Reinstate the tests below when all metadata documents have the new fields and are being updated in real time. + it.skip("should open Sort Work tab and test sorting by group", () => { const students = ["student:1", "student:2", "student:3", "student:4"]; const studentProblemDocs = [ @@ -172,9 +279,9 @@ describe('SortWorkView Tests', () => { sortWork.checkDocumentInGroup("No Group", exemplarDocs[0]); cy.log("check that problem and exemplar documents can be sorted by name"); - sortWork.getSortByMenu().click(); + sortWork.getPrimarySortByMenu().click(); cy.wait(1000); - sortWork.getSortByNameOption().click(); + sortWork.getPrimarySortByNameOption().click(); sortWork.checkSectionHeaderLabelsExist([ "1, Student", "1, Teacher", "2, Student", "3, Student", "4, Student", "Idea, Ivan" ]); @@ -183,9 +290,9 @@ describe('SortWorkView Tests', () => { sortWork.checkDocumentInGroup("1, Student", studentProblemDocs[0]); cy.log("check that exemplar document is displayed in strategy tag sourced from CMS"); - sortWork.getSortByMenu().click(); + sortWork.getPrimarySortByMenu().click(); cy.wait(1000); - sortWork.getSortByTagOption().click(); + sortWork.getPrimarySortByTagOption().click(); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.checkDocumentInGroup("Unit Rate", exemplarDocs[0]); @@ -198,10 +305,10 @@ describe('SortWorkView Tests', () => { chatPanel.getChatCloseButton().click(); cy.openTopTab('sort-work'); // at the moment this is required to refresh the sort - sortWork.getSortByMenu().click(); - sortWork.getSortByNameOption().click(); - sortWork.getSortByMenu().click(); - sortWork.getSortByTagOption().click(); + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByNameOption().click(); + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByTagOption().click(); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.checkDocumentInGroup("Diverging Designs", exemplarDocs[0]); @@ -215,8 +322,8 @@ describe('SortWorkView Tests', () => { cy.openTopTab('sort-work'); cy.log("check that exemplar document is still displayed in strategy tag sourced from CMS but not in teacher added tag"); - sortWork.getSortByMenu().click(); - sortWork.getSortByTagOption().click(); + sortWork.getPrimarySortByMenu().click(); + sortWork.getPrimarySortByTagOption().click(); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.checkDocumentInGroup("Unit Rate", exemplarDocs[0]); sortWork.checkGroupIsEmpty("Diverging Designs"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_starreddocument_scroller_spec.js b/cypress/e2e/functional/teacher_tests/teacher_starreddocument_scroller_spec.js index 6027e2e1d6..26dc283863 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_starreddocument_scroller_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_starreddocument_scroller_spec.js @@ -6,7 +6,6 @@ let starred = new StarredTab; function beforeTest() { const queryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_support_spec.js b/cypress/e2e/functional/teacher_tests/teacher_support_spec.js index eb628afc4a..b1957ab228 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_support_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_support_spec.js @@ -8,13 +8,12 @@ const teacherQueryParams = `${Cypress.config("qaUnitTeacher6")}`; const studentQueryParams = `${Cypress.config("qaUnitStudent5")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); cy.wait(4000); } - + function loadStudentSession(params) { cy.visit(params); cy.waitForLoad(); @@ -36,7 +35,7 @@ context('Teacher Support', function() { cy.openTopTab("class-work"); cy.openSection('class-work','workspaces'); resourcesPanel.getCanvasItemTitle('class-work','workspaces').should('contain',title); - + cy.log('verify teacher support is visible in student nav'); loadStudentSession(studentQueryParams); cy.openTopTab("class-work"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_tagged_comments_spec.js b/cypress/e2e/functional/teacher_tests/teacher_tagged_comments_spec.js index e0c57186b1..389c8a40d4 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_tagged_comments_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_tagged_comments_spec.js @@ -4,7 +4,6 @@ let chatPanel = new ChatPanel; function beforeTest() { const queryParams = `${Cypress.config("qaUnitTeacher6Network")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.openTopTab("problems"); diff --git a/cypress/e2e/functional/teacher_tests/teacher_workspace_spec.js b/cypress/e2e/functional/teacher_tests/teacher_workspace_spec.js index 3ba2c8cefd..34ce263c6a 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_workspace_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_workspace_spec.js @@ -18,7 +18,6 @@ const teacherQueryParams = `${Cypress.config("qaUnitTeacher6")}`; const studentWorkspaceQueryParams = `${Cypress.config("clueTestqaUnitTeacher6")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); dashboard.switchView("Workspace & Resources"); diff --git a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js index c01b95c26a..52c59cfa50 100644 --- a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js +++ b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js @@ -39,7 +39,6 @@ const queryParamsQa = `${Cypress.config("qaUnitStudent7Investigation3")}`; // so cypress+chrome simply cannot scroll the container. function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); cy.showOnlyDocumentWorkspace(); @@ -134,6 +133,22 @@ context('Arrow Annotations (Sparrows)', function () { aa.getAnnotationButtons().eq(1).click({ force: true }); aa.getAnnotationArrows().should("have.length", 2); + // Short click on the "drag handle" of existing sparrow can create a new sparrow + aa.getAnnotationArrowDragHandles().should('have.length', 4); + aa.getAnnotationArrowDragHandles().eq(0).trigger('mousedown', { force: true }); + aa.getAnnotationArrowDragHandles().eq(0).trigger('mouseup', { force: true }); + aa.getPreviewArrow().should("exist"); + aa.getAnnotationButtons().eq(2).click({ force: true }); + aa.getAnnotationArrows().should("have.length", 3); + aa.getPreviewArrow().should("not.exist"); + aa.getAnnotationDeleteButtons().eq(2).click(); + + // Long click or drag, however, does not create a new sparrow. + aa.getAnnotationArrowDragHandles().eq(3).trigger('mousedown', { force: true }); + cy.wait(500); + aa.getAnnotationArrowDragHandles().eq(3).trigger('mouseup', { force: true }); + aa.getPreviewArrow().should("not.exist"); + cy.log("Can select arrows"); // Click to select aa.getAnnotationSparrowGroups().should("not.have.class", "selected"); @@ -231,13 +246,29 @@ context('Arrow Annotations (Sparrows)', function () { aa.getAnnotationSvg().click(500, 100); aa.getAnnotationButtons().eq(1).click(); // Second end is anchored to an object aa.getAnnotationArrows().should("have.length", 2); - aa.getAnnotationDeleteButtons().eq(0).click(); aa.getAnnotationDeleteButtons().eq(0).click(); + aa.getAnnotationSvg().click(200, 200); // Both ends free should not create an arrow aa.getAnnotationSvg().click(300, 100); aa.getAnnotationArrows().should("have.length", 0); + // Attempting to connect both ends to objects results in second end being free + aa.getAnnotationButtons().eq(0).click(); + aa.getAnnotationButtons().eq(1).click(); // Just the click location is used. + aa.getAnnotationModeButton().click(); // exit sparrow mode + aa.getAnnotationArrows().should("have.length", 1); + drawToolTile.getEllipseDrawing().click({ force: true, scrollBehavior: false }); + clueCanvas.clickToolbarButton('drawing', 'delete'); // delete the object under the second end; arrow should remain since it was not attached. + aa.getAnnotationArrows().should("have.length", 1); + drawToolTile.getRectangleDrawing().eq(0).click({ force: true, scrollBehavior: false }); + clueCanvas.clickToolbarButton('drawing', 'delete'); // delete the object under the first end; arrow should be deleted. + aa.getAnnotationArrows().should("have.length", 0); + + // put the two deleted objects back + drawToolTile.drawRectangle(50, 50); + drawToolTile.drawEllipse(200, 50); + aa.getAnnotationMenuExpander().click(); aa.getCurvedArrowToolbarButton().click(); clueCanvas.getSelectTool().click(); @@ -245,11 +276,7 @@ context('Arrow Annotations (Sparrows)', function () { cy.log("Can create sparrows across two tiles"); clueCanvas.addTile("drawing"); drawToolTile.getDrawTile().should("have.length", 2); - drawToolTile.getDrawToolVector().eq(0).click(); - drawToolTile.getDrawTile().eq(1) - .trigger("pointerdown", 150, 50) - .trigger("pointermove", 100, 150) - .trigger("pointerup", 100, 50); + drawToolTile.drawVector(100, 50, 50, 100); aa.getAnnotationModeButton().click(); aa.getAnnotationButtons().should("have.length", 4); aa.getAnnotationButtons().first().click({ force: true }); @@ -283,7 +310,7 @@ context('Arrow Annotations (Sparrows)', function () { cy.log("Can duplicate annotations contained within one tile"); aa.getAnnotationModeButton().click(); - tableToolTile.getTableCell().eq(1).click(); + tableToolTile.getTableTile().click(); clueCanvas.getDuplicateTool().click(); aa.getAnnotationModeButton().click(); // To force a rerender of the annotation layer aa.getAnnotationModeButton().click(); @@ -560,7 +587,7 @@ context('Arrow Annotations (Sparrows)', function () { cy.log("New annotations can be made on a recorded program"); aa.getAnnotationModeButton().click(); aa.getAnnotationButtons().should("have.length", 4); - aa.getAnnotationButtons().eq(1).click(); + aa.getAnnotationButtons().eq(1).click({ force: true }); aa.getAnnotationButtons().eq(3).click(); aa.getAnnotationArrows().should("have.length", 2); aa.getAnnotationModeButton().click(); diff --git a/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js new file mode 100644 index 0000000000..f2abdbcd41 --- /dev/null +++ b/cypress/e2e/functional/tile_tests/bar_graph_tile_spec.js @@ -0,0 +1,55 @@ +import ClueCanvas from '../../../support/elements/common/cCanvas'; +import Canvas from '../../../support/elements/common/Canvas'; +import BarGraphTile from '../../../support/elements/tile/BarGraphTile'; + +let clueCanvas = new ClueCanvas, + barGraph = new BarGraphTile; + +// eslint-disable-next-line unused-imports/no-unused-vars +const canvas = new Canvas; + +function beforeTest() { + const queryParams = `${Cypress.config("qaUnitStudent5")}`; + cy.visit(queryParams); + cy.waitForLoad(); + cy.showOnlyDocumentWorkspace(); +} + +context('Bar Graph Tile', function () { + + it('Can create tile', function () { + beforeTest(); + + clueCanvas.addTile('bargraph'); + barGraph.getTiles().should('have.length', 1); + barGraph.getTile() + .should('be.visible') + .and('have.class', 'bar-graph-tile') + .and('not.have.class', 'read-only'); + + barGraph.getTileTitle().should("be.visible").and('have.text', 'Bar Graph 1'); + barGraph.getYAxisLabel().should('have.text', 'Counts'); + barGraph.getXAxisPulldownButton(0).should('have.text', 'date'); + }); + + it('Can edit Y axis label', function () { + beforeTest(); + clueCanvas.addTile('bargraph'); + barGraph.getYAxisLabel().should('have.text', 'Counts'); + barGraph.getYAxisLabelEditor().should('not.exist'); + barGraph.getYAxisLabelButton().click(); + barGraph.getYAxisLabelEditor().should('be.visible').type(' of something{enter}'); + barGraph.getYAxisLabelEditor().should('not.exist'); + barGraph.getYAxisLabel().should('have.text', 'Counts of something'); + }); + + it('Can change primary category', function () { + beforeTest(); + clueCanvas.addTile('bargraph'); + barGraph.getXAxisPulldown().should('have.text', 'date'); + barGraph.getXAxisPulldownButton().click(); + barGraph.getXAxisPulldownMenuItem().eq(1).click(); + barGraph.getXAxisPulldown().should('have.text', 'location'); + }); + +}); diff --git a/cypress/e2e/functional/tile_tests/data_card_tool_spec.js b/cypress/e2e/functional/tile_tests/data_card_tool_spec.js index 2801af6631..c7f18cadf1 100644 --- a/cypress/e2e/functional/tile_tests/data_card_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/data_card_tool_spec.js @@ -8,7 +8,6 @@ let xyplot = new XYPlotToolTile; function beforeTest() { const queryParams = `${Cypress.config("qaMothPlotUnitStudent5")}&mouseSensor`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/datacard_merge_spec.js b/cypress/e2e/functional/tile_tests/datacard_merge_spec.js index 1ed259093a..e98d090247 100644 --- a/cypress/e2e/functional/tile_tests/datacard_merge_spec.js +++ b/cypress/e2e/functional/tile_tests/datacard_merge_spec.js @@ -6,11 +6,17 @@ let dc = new DataCardToolTile; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } +function startFresh() { + cy.clearAllSessionStorage(); + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + beforeTest(); +} + context('Merge Data Card Tool Tile', function () { it("Merge Data Card Tool Tile", () => { beforeTest(); @@ -43,7 +49,7 @@ context('Merge Data Card Tool Tile', function () { dc.getAttrValue(1).eq(1).invoke("val").should("contain", "cat"); cy.log("merges two empty Data Card tool tiles"); - beforeTest(); + startFresh(); clueCanvas.addTile("datacard"); dc.getTile(0).should("exist"); dc.getTileTitle(0).should("have.text", "Card Deck Data 1"); @@ -70,7 +76,7 @@ context('Merge Data Card Tool Tile', function () { dc.getCardNofTotalListing(1).should("have.text", "Card 1 of 2"); cy.log("merges filled-in into empty Data Card tool tile"); - beforeTest(); + startFresh(); clueCanvas.addTile("datacard"); dc.getTile(0).should("exist"); dc.getTileTitle(0).should("have.text", "Card Deck Data 1"); @@ -109,7 +115,7 @@ context('Merge Data Card Tool Tile', function () { dc.getCardNofTotalListing(1).should("have.text", "Card 1 of 2"); cy.log("merges empty into filled-in Data Card tool tile"); - beforeTest(); + startFresh(); clueCanvas.addTile("datacard"); dc.getTile(0).should("exist"); dc.getTileTitle(0).should("have.text", "Card Deck Data 1"); @@ -148,7 +154,7 @@ context('Merge Data Card Tool Tile', function () { dc.getCardNofTotalListing(1).should("have.text", "Card 1 of 2"); cy.log("merges two filled-in Data Card tool tiles"); - beforeTest(); + startFresh(); clueCanvas.addTile("datacard"); dc.getTile(0).should("exist"); dc.getTileTitle(0).should("have.text", "Card Deck Data 1"); @@ -191,7 +197,7 @@ context('Merge Data Card Tool Tile', function () { dc.getCardNofTotalListing(1).should("have.text", "Card 1 of 2"); cy.log("merges datacards with same attribute labels"); - beforeTest(); + startFresh(); clueCanvas.addTile("datacard"); dc.getTile(0).should("exist"); dc.getTileTitle(0).should("have.text", "Card Deck Data 1"); diff --git a/cypress/e2e/functional/tile_tests/diagram_tool_spec.js b/cypress/e2e/functional/tile_tests/diagram_tool_spec.js index 23781901a4..bf18c11bb6 100644 --- a/cypress/e2e/functional/tile_tests/diagram_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/diagram_tool_spec.js @@ -14,7 +14,6 @@ const redoKeystroke = `{${cmdKey}}{shift}z`; function beforeTest() { const queryParams = `${Cypress.config("qaVariablesUnitStudent5")}&mouseSensor`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.showOnlyDocumentWorkspace(); diff --git a/cypress/e2e/functional/tile_tests/drawing_tool_spec.js b/cypress/e2e/functional/tile_tests/drawing_tool_spec.js index 9529daddeb..447b96ca2b 100644 --- a/cypress/e2e/functional/tile_tests/drawing_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/drawing_tool_spec.js @@ -9,8 +9,6 @@ const imageToolTile = new ImageToolTile; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); - cy.visit(queryParams); cy.waitForLoad(); cy.showOnlyDocumentWorkspace(); diff --git a/cypress/e2e/functional/tile_tests/duplicate_tile_spec.js b/cypress/e2e/functional/tile_tests/duplicate_tile_spec.js index 931e17905c..58f5940429 100644 --- a/cypress/e2e/functional/tile_tests/duplicate_tile_spec.js +++ b/cypress/e2e/functional/tile_tests/duplicate_tile_spec.js @@ -14,8 +14,6 @@ let clueCanvas = new ClueCanvas, function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); - cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/expression_tool_spec.js b/cypress/e2e/functional/tile_tests/expression_tool_spec.js index fe276a00da..c7c5104283 100644 --- a/cypress/e2e/functional/tile_tests/expression_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/expression_tool_spec.js @@ -6,7 +6,6 @@ let exp = new ExpressionToolTile; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); //TODO - implement within a curriculum unit diff --git a/cypress/e2e/functional/tile_tests/geometry_table_integraton_test_spec.js b/cypress/e2e/functional/tile_tests/geometry_table_integraton_test_spec.js index 2126a185d3..95cefec597 100644 --- a/cypress/e2e/functional/tile_tests/geometry_table_integraton_test_spec.js +++ b/cypress/e2e/functional/tile_tests/geometry_table_integraton_test_spec.js @@ -15,8 +15,6 @@ const y = ['2.5', '5', '1', '0']; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); - cy.visit(queryParams); cy.waitForLoad(); clueCanvas.getInvestigationCanvasTitle().text().as('investigationTitle'); diff --git a/cypress/e2e/functional/tile_tests/geometry_tool_spec.js b/cypress/e2e/functional/tile_tests/geometry_tool_spec.js index 32b9e8e0c3..c053b75ae8 100644 --- a/cypress/e2e/functional/tile_tests/geometry_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/geometry_tool_spec.js @@ -18,7 +18,6 @@ const ptsDoc = 'Points'; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.collapseResourceTabs(); @@ -167,7 +166,6 @@ context('Geometry Tool', function () { geometryToolTile.selectGraphPoint(1, 1); geometryToolTile.getGraphPoint().eq(0).should("have.attr", "fill", "#0069ff"); // $data-blue geometryToolTile.getSelectedGraphPoint().should("have.length", 1); - // set label options geometryToolTile.getGraphPointLabel().contains('A').should('not.exist'); clueCanvas.clickToolbarButton('geometry', 'label'); @@ -206,27 +204,20 @@ context('Geometry Tool', function () { geometryToolTile.getPhantomGraphPoint().should("have.length", 1); // Create vertex angle - cy.log('Create vertex angle'); - clueCanvas.clickToolbarButton('geometry', 'select'); geometryToolTile.getGraphPointLabel().contains('90°').should('not.exist'); clueCanvas.clickToolbarButton('geometry', 'select'); geometryToolTile.selectGraphPoint(10, 5); // this point is a 90 degree angle clueCanvas.clickToolbarButton('geometry', 'label'); geometryToolTile.toggleAngleCheckbox(); geometryToolTile.getGraphPointLabel().contains('90°').should('exist'); - clueCanvas.clickToolbarButton('geometry', 'label'); - geometryToolTile.toggleAngleCheckbox(); - geometryToolTile.getGraphPointLabel().contains('90°').should('not.exist'); - //Label the polygon - cy.log('label the polygon'); - clueCanvas.clickToolbarButton('geometry', 'select'); + // Label the polygon geometryToolTile.getGraphPolygon().click(50, 50, { force: true, }); geometryToolTile.getSelectedGraphPoint().should('have.length', 3); geometryToolTile.getGraphPointLabel().contains('12.').should('not.exist'); geometryToolTile.getGraphPointLabel().contains('ABC').should('not.exist'); clueCanvas.clickToolbarButton('geometry', 'label'); - geometryToolTile.getModalTitle().should('include.text', 'Label'); + geometryToolTile.getModalTitle().should('contain.text', 'Polygon Label/Value'); geometryToolTile.chooseLabelOption('length'); geometryToolTile.getGraphPointLabel().contains('12.').should('exist'); clueCanvas.clickToolbarButton('geometry', 'label'); @@ -239,7 +230,6 @@ context('Geometry Tool', function () { geometryToolTile.clickGraphPosition(0, 0); // deselect polygon // Label a segment - cy.log('label a segment'); geometryToolTile.getGraphPointLabel().contains('AB').should('not.exist'); geometryToolTile.getGraphLine().should('have.length', 5); // 0-1 = axis lines, 2-4 = triangle geometryToolTile.getGraphLine().eq(4).click({ force: true }); @@ -255,77 +245,6 @@ context('Geometry Tool', function () { geometryToolTile.chooseLabelOption('none'); geometryToolTile.getGraphPointLabel().contains('AB').should('not.exist'); geometryToolTile.getGraphPointLabel().contains('5').should('not.exist'); - geometryToolTile.clickGraphPosition(0, 0); // deselect the segment - - // Test keyboard functions to move the selected point(s) - cy.log('Test keyboard functions to move polygon points'); - - // turn on 90 degree angle for check - clueCanvas.clickToolbarButton('geometry', 'select'); - geometryToolTile.selectGraphPoint(10, 5); // this point is a 90 degree angle - clueCanvas.clickToolbarButton('geometry', 'label'); - geometryToolTile.toggleAngleCheckbox(); - geometryToolTile.getGraphPointLabel().contains('90°').should('exist'); - - // Select the graph point at (5, 5) - clueCanvas.clickToolbarButton('geometry', 'select'); - geometryToolTile.clickGraphPosition(5, 5); - - // Verify that the point has been selected - geometryToolTile.getSelectedGraphPoint().should('have.length', 1); - - // Store the original coordinates for comparison - let originalXCoord, originalYCoord; - geometryToolTile.getSelectedGraphPoint().then(($point) => { - originalXCoord = parseFloat($point.attr('cx')); - originalYCoord = parseFloat($point.attr('cy')); - }); - - // Move the selected point up using the arrow key - clueCanvas.clickToolbarButton('geometry', 'select'); - geometryToolTile.clickGraphPosition(5, 5); // shared point - geometryToolTile.getSelectedGraphPoint().trigger('keydown', { keyCode: 38 }); // simulate up arrow key press - - // Move the selected point right using the arrow key - geometryToolTile.getSelectedGraphPoint().trigger('keydown', { keyCode: 39 }); // simulate right arrow key press - - // Verify that the point has moved: cx should be greater and cy should be less than the original values - geometryToolTile.getSelectedGraphPoint().then(($point) => { - const newXCoord = parseFloat($point.attr('cx')); - const newYCoord = parseFloat($point.attr('cy')); - - expect(newXCoord).to.be.greaterThan(originalXCoord); - expect(newYCoord).to.be.lessThan(originalYCoord); - }); - - // Also check that the angle label has changed from its original value - geometryToolTile.getAngleAdornment().should(($label) => { - const angleText = $label.text(); - expect(angleText).not.to.equal('90'); // 90° was the original value - }); - - // Move the point back to the original position - geometryToolTile.getSelectedGraphPoint().trigger('keydown', { keyCode: 37 }); // simulate left arrow key press - geometryToolTile.getSelectedGraphPoint().trigger('keydown', { keyCode: 40 }); // simulate down arrow key press - - // Verify that the point has returned to its original coordinates - geometryToolTile.getSelectedGraphPoint().then(($point) => { - const newXCoord = parseFloat($point.attr('cx')); - const newYCoord = parseFloat($point.attr('cy')); - - expect(newXCoord).to.equal(originalXCoord); - expect(newYCoord).to.equal(originalYCoord); - }); - - // Verify that the angle label returns to its original value - geometryToolTile.getGraphPointLabel().contains('90°').should('exist'); - - // Turn off 90 degree angle label for check - clueCanvas.clickToolbarButton('geometry', 'select'); - geometryToolTile.selectGraphPoint(10, 5); // this point is a 90 degree angle - clueCanvas.clickToolbarButton('geometry', 'label'); - geometryToolTile.toggleAngleCheckbox(); - geometryToolTile.getGraphPointLabel().should('not.contain', '90°'); // Change color of polygon geometryToolTile.selectGraphPoint(7, 6); // click middle of polygon to select it @@ -584,7 +503,6 @@ context('Geometry Tool', function () { clueCanvas.clickToolbarButton('geometry', 'delete'); geometryToolTile.getGraphCircle().should("have.length", 0); geometryToolTile.getGraphPoint().should("have.length", 1); - geometryToolTile.getGraphPolygon().should("have.length", 0); }); it('will test Geometry tile undo redo', () => { diff --git a/cypress/e2e/functional/tile_tests/image_tool_spec.js b/cypress/e2e/functional/tile_tests/image_tool_spec.js index 94bb56ecf5..6c68907555 100644 --- a/cypress/e2e/functional/tile_tests/image_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/image_tool_spec.js @@ -10,7 +10,6 @@ let userCanvas = 'Uploaded Images'; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.showOnlyDocumentWorkspace(); diff --git a/cypress/e2e/functional/tile_tests/numberline_tool_spec.js b/cypress/e2e/functional/tile_tests/numberline_tool_spec.js index 335ff2ba01..784798a33f 100644 --- a/cypress/e2e/functional/tile_tests/numberline_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/numberline_tool_spec.js @@ -6,7 +6,6 @@ let numberlineToolTile = new NumberlineToolTile; function beforeTest() { const queryParams = `${Cypress.config("qaNoNavPanelUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/shared_dataset_spec.js b/cypress/e2e/functional/tile_tests/shared_dataset_spec.js index 83fd6a7d05..3988d93152 100644 --- a/cypress/e2e/functional/tile_tests/shared_dataset_spec.js +++ b/cypress/e2e/functional/tile_tests/shared_dataset_spec.js @@ -11,7 +11,6 @@ let xyTile = new XYPlotTile; const queryParams = `${Cypress.config("qaNoNavPanelUnitStudent5")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/shared_variables_test_spec.js b/cypress/e2e/functional/tile_tests/shared_variables_test_spec.js index 934a7b4c00..676c12ae7b 100644 --- a/cypress/e2e/functional/tile_tests/shared_variables_test_spec.js +++ b/cypress/e2e/functional/tile_tests/shared_variables_test_spec.js @@ -10,7 +10,6 @@ const diagramToolTile = new DiagramToolTile; function beforeTest() { const queryParam = `${Cypress.config("qaVariablesUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParam); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/simulator_tile_spec.js b/cypress/e2e/functional/tile_tests/simulator_tile_spec.js index 8c6f4c0321..d9ff5e7e9c 100644 --- a/cypress/e2e/functional/tile_tests/simulator_tile_spec.js +++ b/cypress/e2e/functional/tile_tests/simulator_tile_spec.js @@ -11,7 +11,6 @@ const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitStudent5")}`; const queryParams3 = `${Cypress.config("qaUnitStudent7Investigation3")}`; function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/table_tool_spec.js b/cypress/e2e/functional/tile_tests/table_tool_spec.js index 7f6f1007a4..3583006d83 100644 --- a/cypress/e2e/functional/tile_tests/table_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/table_tool_spec.js @@ -19,7 +19,6 @@ let copyTitle = 'Table Tile Workspace Copy'; function beforeTest() { const queryParams = `${Cypress.config("qaNoNavPanelUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); cy.showOnlyDocumentWorkspace(); diff --git a/cypress/e2e/functional/tile_tests/text_tool_spec.js b/cypress/e2e/functional/tile_tests/text_tool_spec.js index 2111cf8127..8081fa319c 100644 --- a/cypress/e2e/functional/tile_tests/text_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/text_tool_spec.js @@ -10,7 +10,6 @@ let copyTitle = 'Text Tile Workspace Copy'; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js index 1e8d472bbe..ac69f7f775 100644 --- a/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js +++ b/cypress/e2e/functional/tile_tests/xy_plot_tool_spec.js @@ -58,7 +58,6 @@ function attributeOfTransform(matcher, n) { function beforeTest(params) { - cy.clearQAData('all'); cy.visit(params); cy.waitForLoad(); } diff --git a/cypress/e2e/smoke/single_student_canvas_test.js b/cypress/e2e/smoke/single_student_canvas_test.js index 99bff1ac36..36949702bd 100644 --- a/cypress/e2e/smoke/single_student_canvas_test.js +++ b/cypress/e2e/smoke/single_student_canvas_test.js @@ -18,7 +18,6 @@ const title = "QA 1.1 Solving a Mystery with Proportional Reasoning"; function beforeTest() { const queryParams = `${Cypress.config("qaUnitStudent5")}`; - cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); } diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3a4676718c..bd8806d141 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -52,21 +52,6 @@ Cypress.Commands.add("uploadFile",(selector, filename, type="")=>{ }); }); }); -Cypress.Commands.add("clearQAData", (data)=>{ //clears data from Firebase (currently data='all' is the only one supported) - if (data==='all') { - - cy.visit('?appMode=qa&qaClear=' + data + '&fakeClass=5&fakeUser=student:5'); - // For some reason when using - // cy.get('span', {timeout: 60000}).should('contain','QA Cleared: OK'); - // If there is a test failure then a weird - // error is shown: - // object tested must be an array, a map, an object, a set, a string, - // or a weakset, but undefined given - // The log shows the assertion passing and then shows it failing right after - // using contains fixes this problem. - cy.contains('span', 'QA Cleared: OK', {timeout: 60000}); - } -}); // Login using cy.request, this is faster than using visit, and it makes it possible // to visit a local domain after logging in @@ -117,6 +102,10 @@ Cypress.Commands.add("launchReport", (reportUrl) => { }); Cypress.Commands.add("waitForLoad", () => { cy.get('.version', {timeout: 60000}); + // Log the firebase user id + cy.window().its('stores.db.firebase.userId').then(id => { + cy.log("Firebase uid", id); + }); }); Cypress.Commands.add("deleteWorkspaces",(baseUrl,queryParams)=>{ let primaryWorkspace = new PrimaryWorkspace; diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 1ec343511f..8de7870dd5 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -28,10 +28,6 @@ import './commands'; import installLogsCollector from "cypress-terminal-report/src/installLogsCollector"; installLogsCollector(); -// before(function(){ //Can't run this because full tests will not run due to website switching -// cy.clearQAData('all'); -// }); - Cypress.on('uncaught:exception', (err, runnable) => { // returning false here prevents Cypress from // failing the test @@ -54,7 +50,3 @@ if (Cypress.config('hideXHRInCommandLog')) { app.document.head.appendChild(style); } } - -after(function(){ - cy.clearQAData('all'); -}); diff --git a/cypress/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index 07bd02f01e..dd02b32ed8 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -1,15 +1,21 @@ class SortedWork { - getSortByMenu() { - return cy.get('.custom-select.sort-work-sort-menu'); + getPrimarySortByMenu() { + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu'); } - getSortByNameOption() { - return cy.get('[data-test="list-item-name"]'); + getPrimarySortByNameOption() { + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu [data-test="list-item-name"]'); } - getSortByGroupOption() { - return cy.get('[data-test="list-item-group"]'); + getPrimarySortByGroupOption() { + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu [data-test="list-item-group"]'); } - getSortByTagOption(){ - return cy.get('[data-test="list-item-identify-design approach"]'); + getPrimarySortByTagOption(){ + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu [data-test="list-item-identify-design approach"]'); + } + getPrimarySortByBookmarkedOption(){ + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu [data-test="list-item-bookmarked"]'); + } + getPrimarySortByToolsOption(){ + return cy.get('.custom-select.sort-work-sort-menu.primary-sort-menu [data-test="list-item-tools"]'); } getSortWorkItem() { return cy.get(".sort-work-view .sorted-sections .list-item .footer .info"); @@ -20,6 +26,46 @@ class SortedWork { getSortWorkGroup(groupName) { return cy.get(".sort-work-view .sorted-sections .section-header-label").contains(groupName).parent().parent().parent(); } + getSortWorkSubgroup(groupName, subgroupName) { + return this.getSortWorkGroup(groupName) + .find('[data-testid="doc-group"] [data-testid="doc-group-label"]').contains(subgroupName).parent(); + } + getSecondarySortByMenu() { + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu'); + } + getSecondarySortByNoneOption() { + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-none"]'); + } + getSecondarySortByNameOption() { + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-name"]'); + } + getSecondarySortByGroupOption() { + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-group"]'); + } + getSecondarySortByTagOption(){ + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-identify-design approach"]'); + } + getSecondarySortByBookmarkedOption(){ + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-bookmarked"]'); + } + getSecondarySortByToolsOption(){ + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu [data-test="list-item-tools"]'); + } + getShowForMenu() { + return cy.get("[data-test=filter-work-menu]"); + } + getShowForProblemOption() { + return cy.get("[data-test=list-item-problem]"); + } + getShowForInvestigationOption() { + return cy.get("[data-test=list-item-investigation]"); + } + getShowForUnitOption() { + return cy.get("[data-test=list-item-unit]"); + } + getShowForAllOption() { + return cy.get("[data-test=list-item-all]"); + } openSortWorkSection(sectionLabel) { return cy.get(".sort-work-view .sorted-sections .section-header-label").contains(sectionLabel).get(".section-header-right .section-header-arrow").click({multiple: true}); } @@ -29,6 +75,32 @@ class SortedWork { checkDocumentNotInGroup(groupName, doc) { this.getSortWorkGroup(groupName).find(".list .list-item .footer .info").should("not.contain", doc); } + checkSimpleDocumentInGroup(groupName, doc) { + this.getSortWorkGroup(groupName).find('[data-testid="section-document-list"] [data-test="simple-document-item"]').should("have.attr", "title", doc); + } + checkSimpleDocumentInSubgroup(groupName, subgroupName, doc) { + this.getSortWorkSubgroup(groupName, subgroupName).find('[data-test="simple-document-item"]').should("have.attr", "title", doc); + } + checkGroupDocumentVisibility(groupName, isPrivate, isThumbnailView = false) { + const docSelector = isThumbnailView + ? '[data-test="sort-work-list-items"]' + : '[data-testid="doc-group-list"] [data-test="simple-document-item"]'; + + // Assign the documents list to a variable to simplify the code + cy.get(".section-header-left").contains(groupName).parent().parent() + .siblings('[data-testid="section-document-list"]') + .within(() => { + cy.get(docSelector).as("groupDocs"); + }); + + cy.get("@groupDocs").should(`${isPrivate ? "" : "not."}have.class`, "private"); + cy.get("@groupDocs").first().click(); + cy.get(".focus-document").should(`${isPrivate ? "not." : ""}exist`); + + if (!isPrivate) { + cy.get(".close-doc-button").click(); + } + } checkGroupIsEmpty(groupName){ cy.get(".sort-work-view .sorted-sections .section-header-label") .contains(groupName).parent().parent().parent().find(".list").should('be.empty'); diff --git a/cypress/support/elements/tile/ArrowAnnotation.js b/cypress/support/elements/tile/ArrowAnnotation.js index 6ed7bc4e9d..f855e4d62a 100644 --- a/cypress/support/elements/tile/ArrowAnnotation.js +++ b/cypress/support/elements/tile/ArrowAnnotation.js @@ -30,6 +30,9 @@ class ArrowAnnotation { getAnnotationArrows(workspaceClass) { return cy.get(`${wsClass(workspaceClass)} .annotation-layer .annotation-svg .arrow.foreground-arrow`); } + getAnnotationArrowDragHandles(workspaceClass) { + return cy.get(`${wsClass(workspaceClass)} .annotation-layer .annotation-svg .drag-handle`); + } getAnnotationBackgroundArrowPaths(workspaceClass) { return cy.get(`${wsClass(workspaceClass)} .annotation-layer .annotation-svg .arrow.background-arrow path`); } diff --git a/cypress/support/elements/tile/BarGraphTile.js b/cypress/support/elements/tile/BarGraphTile.js new file mode 100644 index 0000000000..d2943936fb --- /dev/null +++ b/cypress/support/elements/tile/BarGraphTile.js @@ -0,0 +1,40 @@ +class BarGraphTile { + + getTiles(workspaceClass) { + return cy.get(`${workspaceClass || ".primary-workspace"} .canvas-area .bar-graph-tile`); + } + + getTile(tileIndex = 0, workspaceClass) { + return this.getTiles().eq(tileIndex); + } + + getTileTitle(tileIndex = 0, workspaceClass) { + return this.getTile(tileIndex, workspaceClass).find(`.editable-tile-title-text`); + } + + getYAxisLabel(tileIndex = 0, workspaceClass) { + return this.getTile(tileIndex, workspaceClass).find(`.editable-axis-label`); + } + + getYAxisLabelButton(tileIndex = 0, workspaceClass) { + return this.getTile(tileIndex, workspaceClass).find(`[data-testid="axis-label-button"]`); + } + + getYAxisLabelEditor(tileIndex = 0, workspaceClass) { + return this.getTile(tileIndex, workspaceClass).find(`[data-testid="axis-label-editor"] input`); + } + + getXAxisPulldown(tileIndex = 0, workspaceClass) { + return this.getTile(tileIndex, workspaceClass).find(`[data-testid="category-pulldown"]`); + } + + getXAxisPulldownButton(tileIndex = 0, workspaceClass) { + return this.getXAxisPulldown(tileIndex, workspaceClass).find(`button`); + } + + getXAxisPulldownMenuItem(tileIndex = 0, workspaceClass) { + return cy.get(`body .chakra-portal button`).filter(':visible'); + } + +} +export default BarGraphTile; diff --git a/docs/firestore-schema.md b/docs/firestore-schema.md new file mode 100644 index 0000000000..2343a901ec --- /dev/null +++ b/docs/firestore-schema.md @@ -0,0 +1,95 @@ +# Firestore database structure + +## Top level collections + +These are similar to Firebase. + +`authed, demo, dev, qa, tests, users` + +Within `authed`, there are several portal site names: + +- `learn_concord_org` (production) +- `learn_portal_staging_concord_org` +- `learn_staging_concord_org` +- `learn-migrate_concord_org` (not sure of status, has only `documents`) + +Within `demo`, the names of demo spaces (eg, "CLUE") + +Within `dev`, UUIDs of dev instances. + +Within `qa`, UUIDs of test instances. + +`tests` is something different, doc TODO. + +`users` top level collection looks like a mistake. + +## Second level + +Collections within `(authed|demo|dev|qa)/{id}`: + +- classes +- curriculum +- documents +- images +- mcimages +- mcsupports +- offerings +- users + +## Third level + +### Contents of `classes/{classDocId}` + +Currently we are storing class docs under both of these `classDocId`s: + +- classes/{network}_{contextid} +- classes/{contextid} + +Fields: + +- id (string) +- name (string) +- context_id (string, uuid) +- network: (string, name of network) +- teacher: (string, full name of teacher who created it) +- uri: (uri on the portal) +- teachers: (array of IDs of teachers) + +### Contents of `curriculum/{docPath}` + +TODO + +### Contents of `documents/{docId}` + +Fields: + +- key: (string, the id of the document in firebase) +- title: (string) +- type: (string, eg "problem") +- uid: (string). TODO: determine if this is the owner of the document, the owner of the comments, or sometimes either. +- contextId: (currently ignored; see `DocumentModel.metadata()`) +- context_id: (string, uuid, should match context_id of a class) +- createdAt: (timestamp) +- network: (string, name of a network) +- originDoc: (string, if set = key of the original document that created this PublishedDocument) +- properties: (map, eg { pubCount: 1 }) +- teachers: (array of user IDs) _should be removed_ + +Collection: + +- comments +- history + +#### Contents of `documents/{docId}/comments/{commentId}` + +- content +- createdAt (date & time) +- name: (full name) +- network (network name) +- tileId: (string, mobx id) +- uid: (string) +- tags: (array of strings) + +### images, mcimages, mcsupports, offerings, users + +TODO diff --git a/docs/oauth2.md b/docs/oauth2.md index 4280fb4149..895a2355bf 100644 --- a/docs/oauth2.md +++ b/docs/oauth2.md @@ -86,10 +86,8 @@ CLUE uses this domain for somethings. It seems reasonable for the Portal to alwa - figure out a way to handle branches with the OAuth2 redirects, so we don't have to update the portal configuration each time we want to test a new branch. - simplify params used by a report launch of CLUE. With just the resourceLinkId and a domain it can discover all of the information it needs for the report. This makes the report launch more symmetric with the student launch. Especially if the offering api (or perhaps new resourceLinkId api) provided info about class (or context). The biggest problem with using a single id like that is that either we need a dynamic api where we can specify the shape of the result, or we have to make one request to get the offering info, wait for it, and then make a second request to get the class info. The next bullet can be used to simplify this. - update the portal APIs so it is easier for apps like CLUE to get all of the info they need for a teacher and a student via a single request. But note that if the user is a researcher and not a student or teacher in the class, then the response should not include student names. And it should only include students that have consented for their work to be visible to researchers. -- see if we can simplify the app mode calculation. There are currently a few places to use the token to compute the app mode or whether the app is previewing. It would be easier to separate the `qaClear` from the `qa` app mode. Then we wouldn't need to work with the app mode right at the beginning. This way working with the token could be postponed until `initializeApp` which could figure out the `appMode` itself. But the current clear code is making sure the `appMode` is`qa` so it doesn't accidentally clear out production data. - consider changing urlParams approach so it is an interface to the actual URL. This way if the URL is changed then any code accessing the urlParams will get this updated version. Even better would be to make this observable, so components using url parameters would get re-rendered if the parameter was changed. This way the URL is the source of truth instead of some object that was copied from it. -- move all qaClear code out of AppComponent. The `qaClear=all` is handled outside but other types of qaClear (offering and class) are still handled by the AppComponent. -- something simple like qaClear is still downloading all of the javascript needed by CLUE. It looks like it is also downloading the cms libraries, but it is not. These bundles include the common code that both CLUE and the cms code use. To improve the loading time, we'd need to make CLUE loading even more dynamic. So something like qaClear and the initial OAuth2 load would not have to wait for all of the CLUE javascript to be loaded. If we make this change, we probably want to optimize it so the other core files do start downloading right away, but if all we are doing is qaClear or oauth2 redirecting we just don't wait for them. +- something simple like oauth2 redirecting is still downloading all of the javascript needed by CLUE. It looks like it is also downloading the cms libraries, but it is not. These bundles include the common code that both CLUE and the cms code use. To improve the loading time, we'd need to make CLUE loading even more dynamic. So the initial OAuth2 load would not have to wait for all of the CLUE javascript to be loaded. If we make this change, we probably want to optimize it so the other core files start downloading right away, but if all we are doing is oauth2 redirecting we just don't wait for them. - getClassInfo adds the offeringId to the students and teachers in the ClassInfo object it creates. I think it'd be better to remove this offeringId from these user objects in the ClassInfo. Then anything that needs it will need to access it a different a way. The offeringId is basically a global since CLUE is always launched associated with an assignment (offering). The offeringId of the user ends up being set on the UserModel (which comes from the students in teachers in the ClassInfo). From there it is used by `get activityUrl` to find which of the portalClassOfferings is the active one, to find its activityUrl. It is also used by `getOfferingPath`. This should be refactored because a user does not determine an offering and really it doesn't determine a class either. We should fix things to not make this assumption. In db.ts the offeringId from the UserModel is used to setup the group. This would be the perfect place to pull it from a separate store which which isn't specific to the user. It is also used in app.tsx, in this case the offeringId could be taken from a global store also. So the only unknown spot is all of the places that might be calling getOfferingPath. ## Refactoring out offeringId from UserModel diff --git a/firebase-test/src/class-rules.test.ts b/firebase-test/src/class-rules.test.ts index dded8ea400..dca8b3ff17 100644 --- a/firebase-test/src/class-rules.test.ts +++ b/firebase-test/src/class-rules.test.ts @@ -2,7 +2,10 @@ import firebase from "firebase"; import { adminWriteDoc, expectDeleteToFail, expectReadToFail, expectReadToSucceed, expectWriteToFail, expectWriteToSucceed, genericAuth, initFirestore, network1, network2, prepareEachTest, studentAuth, - teacher2Auth, teacher2Id, teacher2Name, teacher3Auth, teacher3Id, teacher3Name, teacherAuth, teacherId, teacherName, + teacher2Auth, teacher2Id, teacher2Name, + teacher3Auth, teacher3Id, teacher3Name, + teacher4Auth, teacher4Id, teacher4Name, + teacherAuth, teacherId, teacherName, tearDownTests, thisClass } from "./setup-rules-tests"; @@ -35,7 +38,12 @@ describe("Firestore security rules for offering (activity) documents", () => { await adminWriteDoc( `authed/myPortal/users/${teacher3Id}`, { uid: teacher3Id, name: teacher3Name, type: "teacher", network: network2, networks: [network2] }); - }); + // teacher 4 is not in a network + // TODO: should have a teacher with no networks array for testing purposes too. + await adminWriteDoc( + `authed/myPortal/users/${teacher4Id}`, + { uid: teacher4Id, name: teacher4Name, type: "teacher", networks: [] }); +}); afterAll(async () => { await tearDownTests(); @@ -73,12 +81,23 @@ describe("Firestore security rules for offering (activity) documents", () => { await expectWriteToFail(db, kClassDocPath, specClass()); }); - it("authenticated teachers can read their own class documents", async () => { + it("authenticated teachers in a network can read their own class documents", async () => { db = initFirestore(teacherAuth); await adminWriteDoc(kClassDocPath, specClass()); await expectReadToSucceed(db, kClassDocPath); }); + it("authenticated teachers without a network can read their own class documents", async () => { + db = initFirestore(teacher4Auth); + await adminWriteDoc(kClassDocPath, specClass({teachers: [teacher4Id]}, ["network"])); + await expectReadToSucceed(db, kClassDocPath); + }); + + it("authenticated teachers can read class documents that don't exist", async () => { + db = initFirestore(teacherAuth); + await expectReadToSucceed(db, kClassDocPath); + }); + it("authenticated teachers can read other class documents in the network", async () => { db = initFirestore(teacher2Auth); await adminWriteDoc(kClassDocPath, specClass()); @@ -131,9 +150,9 @@ describe("Firestore security rules for offering (activity) documents", () => { await expectWriteToFail(db, kClassDocPath, specClass({ teachers: [teacher2Id] })); }); - it("authenticated teachers can't write their own class documents without network", async () => { + it("authenticated teachers can write their own class documents without network", async () => { db = initFirestore(teacherAuth); - await expectWriteToFail(db, kClassDocPath, specClass({}, ["network"])); + await expectWriteToSucceed(db, kClassDocPath, specClass({}, ["network"])); }); it("authenticated teachers can update the name of their own class documents", async () => { @@ -184,12 +203,6 @@ describe("Firestore security rules for offering (activity) documents", () => { await expectWriteToFail(db, kClassDocPath, specClass({ context_id: "better-context-id" })); }); - it("authenticated teachers can't update read-only properties of class documents: network", async () => { - db = initFirestore(teacherAuth); - await adminWriteDoc(kClassDocPath, specClass()); - await expectWriteToFail(db, kClassDocPath, specClass({ network: "better-network" })); - }); - it("authenticated teachers can't delete their own class documents", async () => { db = initFirestore(teacherAuth); await adminWriteDoc(kClassDocPath, specClass()); diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index 86573ee07f..7e7da5d409 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -7,7 +7,8 @@ import { teacher2Auth, teacher2Id, teacher2Name, teacher4Auth, teacher4Id, teacher4Name, teacherAuth, teacherId, teacherName, - tearDownTests, thisClass + tearDownTests, + thisClass } from "./setup-rules-tests"; describe("Firestore security rules", () => { @@ -30,7 +31,7 @@ describe("Firestore security rules", () => { } function specDocumentDoc(options?: ISpecDocumentDoc) { // a valid document specification - const documentDoc = { context_id: thisClass, network: noNetwork, teachers: [teacherId], uid: teacherId, + const documentDoc = { context_id: thisClass, network: noNetwork, uid: teacherId, type: "problemDocument", key: "my-document", createdAt: mockTimestamp() }; // remove specified props for validating the tests that require them options?.remove?.forEach(prop => delete (documentDoc as any)[prop]); @@ -53,6 +54,19 @@ describe("Firestore security rules", () => { { uid: teacher4Id, name: teacher4Name, type: "teacher", network, networks: [network] }); } + const kClassDocPath = `authed/myPortal/classes`; + + async function specClassDoc(classId: string, teacherId: string) { + await adminWriteDoc(`${kClassDocPath}/${classId}`, + { id: classId, + name: 'MyClass', + context_id: classId, + teacher: "Some Teacher", + teachers: [teacherId] + } + ); + } + describe("user documents", () => { it("unauthenticated users can't read authenticated user documents", async () => { db = initFirestore(); @@ -88,13 +102,6 @@ describe("Firestore security rules", () => { await expectReadToSucceed(db, kDocumentDocPath); }); - it("student can tell if document exists, but not read it", async () => { - db = initFirestore(studentAuth); - await expectReadToSucceed(db, kDocumentDocPath); - await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); - await expectReadToFail(db, kDocumentDocPath); - }); - it("generic auth can tell if document exists, but not read it", async () => { db = initFirestore(genericAuth); await expectReadToSucceed(db, kDocumentDocPath); @@ -109,55 +116,125 @@ describe("Firestore security rules", () => { it("authenticated teachers can't write user documents without required uid", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ remove: ["uid"] })); }); - it("authenticated teachers can't write user documents without required network", async () => { - db = initFirestore(teacherAuth); - await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ remove: ["network"] })); - }); - it("authenticated teachers can't write user documents without required type", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ remove: ["type"] })); }); it("authenticated teachers can't write user documents without required key", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ remove: ["key"] })); }); it("authenticated teachers can write user documents", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await expectWriteToSucceed(db, kDocumentDocPath, specDocumentDoc()); }); it("authenticated teachers can update user documents", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); await expectUpdateToSucceed(db, kDocumentDocPath, { title: "new-title" }); }); - it("authenticated teachers can't update user documents' read-only fields", async () => { + it("authenticated teachers can update legacy user documents", async () => { + // Before 8/2024, teachers were listed in documents directly, rather than looked up in the class docs. + db = initFirestore(teacherAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { teachers: [teacherId] }})); + await expectUpdateToSucceed(db, kDocumentDocPath, { title: "new-title" }); + }); + + // Should teachers be able to create documents in other classes that they belong to + // (that is, a class other than the one they logged in with)? + // If so, these tests should be unskipped. + it("authenticated teachers can write user documents in secondary class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await specClassDoc(otherClass, teacherId); + await expectWriteToSucceed(db, kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + }); + + it("authenticated teachers can update user documents in secondary class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await specClassDoc(otherClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + await expectUpdateToSucceed(db, kDocumentDocPath, { title: "new-title" }); + }); + + it("authenticated teachers can't write user documents in unrelated class", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + }); + + it("authenticated teachers can't update user documents in unrelated class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, ({ add: { context_id: otherClass }})); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title" }); + }); + + it("authenticated teachers can't update user documents' read-only uid field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", uid: teacher2Id }); }); + it("authenticated teachers can't update user documents' read-only type field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", type: "LearningLog" }); + }); + + it("authenticated teachers can't update user documents' read-only key field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", key: "my-new-document" }); + }); + + it("authenticated teachers can't update user documents' read-only createdAt field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", createdAt: mockTimestamp() }); + }); + + it("authenticated teachers can't update user documents' read-only context-id field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", context_id: otherClass }); + }); + it("authenticated teachers can't update other teachers' documents", async () => { db = initFirestore(teacher2Auth); + await specClassDoc(thisClass, teacherId); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title" }); }); it("authenticated teachers can delete user documents", async () => { db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); await expectDeleteToSucceed(db, kDocumentDocPath); }); it("authenticated teachers can't delete other teachers' documents", async () => { db = initFirestore(teacher2Auth); + await specClassDoc(thisClass, teacherId); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); await expectDeleteToFail(db, kDocumentDocPath); }); @@ -182,15 +259,51 @@ describe("Firestore security rules", () => { await expectReadToSucceed(db, kDocumentDocPath); }); - it("authenticated students can't read user documents", async () => { + it("authenticated students can read documents in their class", async () => { db = initFirestore(studentAuth); + await expectReadToSucceed(db, kDocumentDocPath); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectReadToSucceed(db, kDocumentDocPath); + }); + + it("authenticated students can't read documents in a different class", async () => { + db = initFirestore(studentAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); await expectReadToFail(db, kDocumentDocPath); }); - it("authenticated students can't write user documents", async () => { + it("authenticated students can create documents in their class", async () => { db = initFirestore(studentAuth); - await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc()); + await expectWriteToSucceed(db, kDocumentDocPath, specDocumentDoc()); + }); + + it("authenticated students can't create documents in a different class", async () => { + db = initFirestore(studentAuth); + await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + }); + + it("authenticated students can update documents in their class", async () => { + db = initFirestore(studentAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToSucceed(db, kDocumentDocPath, { title: "new-title" }); + }); + + it("authenticated students can't update documents in a different class", async () => { + db = initFirestore(studentAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title" }); + }); + + it("authenticated students can't delete documents in their class", async () => { + db = initFirestore(studentAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectDeleteToFail(db, kDocumentDocPath); + }); + + it("authenticated students can't delete documents in a different class", async () => { + db = initFirestore(studentAuth); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + await expectDeleteToFail(db, kDocumentDocPath); }); }); @@ -427,6 +540,15 @@ describe("Firestore security rules", () => { await expectReadToSucceed(db, kDocumentCommentDocPath); }); + it ("teacher can look for comments on a metadata document that doesn't exist", async () => { + db = initFirestore(teacherAuth); + + // In practice this is not going to be a direct comment read. Instead it will be a query + // for the list of comments under the document. However the access check should be the + // same. + await expectReadToSucceed(db, kDocumentCommentDocPath); + }); + it("authenticated teachers can't write document comments without required uid", async () => { await initFirestoreWithUserDocument(teacherAuth); await expectWriteToFail(db, kDocumentCommentDocPath, specCommentDoc({ remove: ["uid"] })); @@ -452,20 +574,31 @@ describe("Firestore security rules", () => { await expectWriteToSucceed(db, kDocumentCommentDocPath, specCommentDoc()); }); + it("authenticated teachers can write comment in secondary class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await specClassDoc(otherClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); + await expectWriteToSucceed(db, kDocumentCommentDocPath, specCommentDoc()); + }); + it("authenticated teachers can't update document comments' read-only uid field", async () => { await initFirestoreWithUserDocument(teacherAuth); await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); await expectUpdateToFail(db, kDocumentCommentDocPath, { content: "A new comment!", uid: teacher2Id }); }); - it("authenticated teachers can't update document comments' read-only network field", async () => { + it("authenticated teachers can update document comments", async () => { await initFirestoreWithUserDocument(teacherAuth); await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); - await expectUpdateToFail(db, kDocumentCommentDocPath, { content: "A new comment!", network: "other-network" }); + await expectUpdateToSucceed(db, kDocumentCommentDocPath, { content: "A new comment!" }); }); - it("authenticated teachers can update document comments", async () => { - await initFirestoreWithUserDocument(teacherAuth); + it("authenticated teachers can update comments in secondary class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await specClassDoc(otherClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); await expectUpdateToSucceed(db, kDocumentCommentDocPath, { content: "A new comment!" }); }); diff --git a/firebase.json b/firebase.json index ea7268ba54..004afc9504 100644 --- a/firebase.json +++ b/firebase.json @@ -20,19 +20,39 @@ "port": 5001 } }, - "functions": { - "predeploy": [ - "npm --prefix \"$RESOURCE_DIR\" run lint", - "npm --prefix \"$RESOURCE_DIR\" run build" - ], - "source": "functions", - "ignore": [ - "*.log", - ".*", - ".git", - "coverage", - "node_modules", - "test" - ] - } + "functions": [ + { + "source": "functions-v1", + "codebase": "functions-v1", + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint", + "npm --prefix \"$RESOURCE_DIR\" run build" + ], + "ignore": [ + "*.log", + ".*", + ".git", + "coverage", + "node_modules", + "test" + ] + }, + { + "source": "functions-v2", + "codebase": "functions-v2", + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint", + "npm --prefix \"$RESOURCE_DIR\" run build" + ], + "ignore": [ + "node_modules", + ".*", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + "*.log", + "coverage" + ] + } + ] } diff --git a/firestore.rules b/firestore.rules index 820616ea69..7f01a54011 100644 --- a/firestore.rules +++ b/firestore.rules @@ -12,7 +12,7 @@ service cloud.firestore { allow read, write: if false; } - function exists(s) { + function stringExists(s) { return (s != null) && (s != ""); } @@ -57,6 +57,7 @@ service cloud.firestore { // user's platform_user_id must be in submitted document's list of teachers function userInRequestTeachers() { return isAuthed() && + 'teachers' in request.resource.data && string(request.auth.token.platform_user_id) in request.resource.data.teachers; } @@ -66,12 +67,6 @@ service cloud.firestore { string(request.auth.token.platform_user_id) == resource.data.uid; } - // user's platform_user_id must be in requested document's list of teachers - function userInResourceTeachers() { - return isAuthed() && - string(request.auth.token.platform_user_id) in resource.data.teachers; - } - // user's class_hash must be in submitted document's list of classes function classInRequestClasses() { return isAuthed() && request.auth.token.class_hash in request.resource.data.classes; @@ -92,21 +87,12 @@ service cloud.firestore { request.resource.data.keys().hasAll(["uid", "unit", "problem", "section", "path", "network"]); } - function isValidDocumentCreateRequest() { - return userInRequestTeachers() && classIsRequestContextId() && - request.resource.data.keys().hasAll(["uid", "network", "type", "key", "createdAt"]); - } - function preservesReadOnlyDocumentFields() { - let readOnlyFieldsSet = ["uid", "network", "type", "key", "createdAt"].toSet(); + let readOnlyFieldsSet = ["uid", "type", "key", "createdAt", "context_id"].toSet(); let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys(); return !affectedFieldsSet.hasAny(readOnlyFieldsSet); } - function isValidDocumentUpdateRequest() { - return userInResourceTeachers() && preservesReadOnlyDocumentFields(); - } - function isValidSupportCreateRequest() { return userIsRequestUser() && classInRequestClasses() && @@ -132,6 +118,31 @@ service cloud.firestore { match /authed/{portal} { allow read, write: if isAuthedTeacher(); + // Check that the class given by the context_id exists and includes the logged-in teacher + function teacherIsInClass(contextid) { + let class_doc = get(/databases/$(database)/documents/authed/$(portal)/classes/$(contextid)); + return class_doc != null && string(request.auth.token.platform_user_id) in class_doc.data.teachers; + } + + // user's platform_user_id must be listed in the class of the requested document + // also allows access to legacy documents which contain their own list of teachers + function userInResourceTeachers() { + return isAuthed() && + ( teacherIsInClass(resource.data.context_id) || + string(request.auth.token.platform_user_id) in resource.data.teachers); + } + + function isValidDocumentCreateRequest() { + return + request.resource.data.keys().hasAll(["uid", "type", "key", "createdAt"]) && + (classIsRequestContextId() || teacherIsInClass(request.resource.data.context_id)); + } + + function isValidDocumentUpdateRequest() { + return preservesReadOnlyDocumentFields() && + ( resourceInUserClass() || userInResourceTeachers() ); + } + // return list of networks available to the current teacher function getTeacherNetworks() { let platformUserId = string(request.auth.token.platform_user_id); @@ -168,13 +179,14 @@ service cloud.firestore { } function isValidClassCreateRequest() { - let requiredFields = ["id", "name", "uri", "context_id", "teacher", "teachers", "network"]; - return userInRequestTeachers() && requestInTeacherNetworks() && - request.resource.data.keys().hasAll(requiredFields); + let requiredFields = ["id", "name", "uri", "context_id", "teacher", "teachers"]; + return userInRequestTeachers() && + (!("network" in request.resource) || requestInTeacherNetworks()) && + request.resource.data.keys().hasAll(requiredFields); } function preservesReadOnlyClassFields() { - let readOnlyFieldsSet = ["id", "uri", "context_id", "network"].toSet(); + let readOnlyFieldsSet = ["id", "uri", "context_id"].toSet(); let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys(); return !affectedFieldsSet.hasAny(readOnlyFieldsSet); } @@ -191,8 +203,11 @@ service cloud.firestore { allow update: if isAuthedTeacher() && isValidClassUpdateRequest(); // we don't support deleting classes at this time allow delete: if false; - // teachers can read their own classes or any class in their network - allow read: if isAuthedTeacher() && (userInRequestTeachers() || resourceInTeacherNetworks()); + // teachers can read, non existing classes, their own classes, or any class in their network + allow read: if isAuthedTeacher() && ( + resource == null || + userInResourceTeachers() || + resourceInTeacherNetworks()); } function isValidOfferingCreateRequest() { @@ -248,7 +263,7 @@ service cloud.firestore { // check whether the (curriculum) document is associated with one of the teacher's networks function curriculumInTeacherNetworks() { let curriculumNetwork = getCurriculumNetwork(); - return exists(curriculumNetwork) && (curriculumNetwork in getTeacherNetworks()); + return stringExists(curriculumNetwork) && (curriculumNetwork in getTeacherNetworks()); } // check whether the teacher owns/created the curriculum document @@ -293,17 +308,21 @@ service cloud.firestore { match /documents/{docId} { // portal-authenticated teachers can create valid documents - allow create: if isAuthedTeacher() && isValidDocumentCreateRequest(); + allow create: if isValidDocumentCreateRequest(); // teachers can only update their own documents and only if they're valid - allow update: if isAuthedTeacher() && isValidDocumentUpdateRequest(); + allow update: if isValidDocumentUpdateRequest(); // teachers can only delete their own documents allow delete: if isAuthedTeacher() && userIsResourceUser(); // teachers can read their own documents or other documents in their network - allow read: if (isAuthed() && (resource == null || userOwnsDocument())) || - (isAuthedTeacher() && (userInResourceTeachers() || resourceInTeacherNetworks() || resourceInUserClass())) + allow read: if (isAuthed() && (resource == null || userOwnsDocument() || resourceInUserClass())) || + (isAuthedTeacher() && (userInResourceTeachers() || resourceInTeacherNetworks())); + + function getDocumentPath() { + return /databases/$(database)/documents/authed/$(portal)/documents/$(docId) + } function getDocumentData() { - return get(/databases/$(database)/documents/authed/$(portal)/documents/$(docId)).data; + return get(getDocumentPath()).data; } // return owner of the parent document @@ -330,18 +349,22 @@ service cloud.firestore { let docData = getDocumentData(); let docNetwork = docData.network; return ( - // check whether the current user is one of the teachers associated with the document - string(request.auth.token.platform_user_id) in docData.teachers || - // check whether the document's network corresponds to one of the users's networks - exists(docNetwork) && (docNetwork in getTeacherNetworks()) || // check if document is in user's class - request.auth.token.class_hash == docData.context_id + request.auth.token.class_hash == docData.context_id || + // check whether the document's network corresponds to one of the users's networks + stringExists(docNetwork) && (docNetwork in getTeacherNetworks()) || + // check whether the document is in a different class for the teacher + teacherIsInClass(docData.context_id) || + // check whether the current user is one of the teachers associated with the (legacy) document + // (listing teachers in the document is no longer current practice) + ('teachers' in docData && + (string(request.auth.token.platform_user_id) in docData.teachers)) ); } // check whether the teacher can access the document function teacherCanAccessDocument() { - return isAuthedTeacher() && userCanAccessDocument(); + return isAuthedTeacher() && (!exists(getDocumentPath()) || userCanAccessDocument()); } function isValidCommentCreateRequest() { diff --git a/functions/.eslintrc.js b/functions-v1/.eslintrc.js similarity index 100% rename from functions/.eslintrc.js rename to functions-v1/.eslintrc.js diff --git a/functions/.gitignore b/functions-v1/.gitignore similarity index 100% rename from functions/.gitignore rename to functions-v1/.gitignore diff --git a/functions-v1/README.md b/functions-v1/README.md new file mode 100644 index 0000000000..4afdd2a84f --- /dev/null +++ b/functions-v1/README.md @@ -0,0 +1,82 @@ +# Firebase Functions +The functions are split into two folders `functions-v1` and `functions-v2`. This folder `functions-v1` contains the legacy functions. We are hoping to incrementally migrate these legacy functions into the newer `functions-v2`. + + +## Available Functions + +|Function|Purpose| +|--------|-------| +|_getImageData_|Retrieves image data that may reside in other classes and hence is not accessible client-side, e.g. for supports published to multiple classes or documents retrieved via the teacher network.| +|_getNetworkDocument_|Retrieves the contents of a document accessible to a teacher via the teacher network.| +|_getNetworkResources_|Retrieves the list of resources (documents) available to a teacher via the teacher network.| +|_postDocumentComment_|Posts a comment to a document in firestore, adding metadata for the document to firestore if necessary.| +|_publishSupport_|Publishes a document as a support that is accessible to all of a teacher's classes (including any referenced images).| +|_validateCommentableDocument_|Checks whether a specific commentable document exists in firestore and creates it if necessary.| + +Here are the basic development operations you can do after you cd into the `functions-v1` directory: +``` +$ cd functions-v1 +$ npm install # install local dependencies +$ npm run lint # lint the functions code +$ npm run test # runs jest (unit) tests for the functions code +$ npm run build # build the functions code (transpile TypeScript) +``` +### Note 1 +There seems to be an uneasy relationship between the `node_modules` folder in the +`functions-v1` directory and the one in the parent directory. I had to explicitly specify the +path to typescript in the `build` function. There's probably a better configuration available, +but in the meantime this seems to mostly work. + +### Note 2 +When running `npm run test` with node 16, the following error is shown +``` +TypeError: Cannot read properties of undefined (reading 'INTERNAL') +``` +This error is triggered by the following line in `test-utils.ts` +``` +import { useEmulators } from "@firebase/rules-unit-testing"; +``` +The current work around is to use node 14 to run the tests. + +See functions/dependency-notes.md for more on this. + +### Testing cloud functions + +Google recommends (requires?) that [firebase-tools](https://www.npmjs.com/package/firebase-tools) be installed globally: +``` +$ npm install -g firebase-tools +``` +This should be run periodically to make sure you're running the latest version of the tools. + +#### Running tests locally (without running functions in the emulator) +``` +$ npm run serve # build and then start the emulators +$ npm run test # run all tests in `functions` directory +$ npm run test -- some.test.ts # run a particular test +``` +The existing tests currently work this way. They test the basic functionality of the cloud functions by importing and calling them directly from node.js test code. This is a simple and efficient way of testing the basic functionality without all the overhead of the functions emulator. The downside is that the node.js test environment is not the same as the hosted function environment. For instance, it's possible to return objects in node.js that can't be JSON-stringified which will throw an error when the function is hosted. That said, you can't beat the convenience of simply calling the functions directly. + +#### Running local tests against functions hosted in the emulator +To run jest tests against functions running in the emulator requires [serving functions using a Cloud Functions Shell](https://firebase.google.com/docs/functions/local-shell#serve_functions_using_a_cloud_functions_shell). Currently, all of our functions are `HTTPS Callable` functions, which [can be called](https://firebase.google.com/docs/functions/local-shell#invoke_https_callable_functions) in this shell mode, but: +>Emulation of context.auth is currently unavailable. + +#### Running CLUE against functions running locally in the emulator: +``` +$ npm run serve # build and then start the functions emulator +``` +and launch CLUE with url parameter `functions=emulator`. + +### To deploy firebase functions to production: +``` +$ npm run deploy # deploy all functions +$ npm run deploy:getImageData # deploy individual function +$ npm run deploy:postDocumentComment # deploy individual function +``` + +By convention, our firebase functions have an internal version number that is returned with any results. This should be incremented appropriately when new versions are deployed. This will allow us to determine whether the current code in GitHub has been deployed or not, for instance. Also by convention, our firebase functions accept parameters of `{ warmUp: true }` which can be issued in advance of any actual call to mitigate the google cloud function cold-start issue. + +### Serving CLUE from https://localhost +To test the deployed function(s) from your local development environment, you may need to run your local dev server with https to avoid CORS errors. To do so, [create a certificate](https://www.matthewhoelter.com/2019/10/21/how-to-setup-https-on-your-local-development-environment-localhost-in-minutes.html) in your `~/.localhost-ssl` directory and name the files `localhost.pem` and `localhost.key`. To use the certificate: +``` +$ npm run start:secure +``` diff --git a/functions/dependencies-notes.md b/functions-v1/dependencies-notes.md similarity index 100% rename from functions/dependencies-notes.md rename to functions-v1/dependencies-notes.md diff --git a/functions/jest.config.js b/functions-v1/jest.config.js similarity index 100% rename from functions/jest.config.js rename to functions-v1/jest.config.js diff --git a/functions/package-lock.json b/functions-v1/package-lock.json similarity index 100% rename from functions/package-lock.json rename to functions-v1/package-lock.json diff --git a/functions/package.json b/functions-v1/package.json similarity index 95% rename from functions/package.json rename to functions-v1/package.json index d3ad117c42..5b27a88056 100644 --- a/functions/package.json +++ b/functions-v1/package.json @@ -1,5 +1,5 @@ { - "name": "functions", + "name": "functions-v1", "scripts": { "lint": "eslint \"src/**/*\" \"test/**/*\"", "build": "npm-run-all clean build:prod", @@ -10,7 +10,7 @@ "serve:functions": "npm run build && firebase emulators:start --only functions", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", - "deploy:all": "firebase deploy --only functions", + "deploy:all": "firebase deploy --only functions:functions-v1", "deploy:getImageData": "firebase deploy --only functions:getImageData_v1", "deploy:getNetworkDocument": "firebase deploy --only functions:getNetworkDocument_v1", "deploy:getNetworkResources": "firebase deploy --only functions:getNetworkResources_v1", diff --git a/functions/src/canonicalize-url.ts b/functions-v1/src/canonicalize-url.ts similarity index 88% rename from functions/src/canonicalize-url.ts rename to functions-v1/src/canonicalize-url.ts index 61d1cc28e8..931d999320 100644 --- a/functions/src/canonicalize-url.ts +++ b/functions-v1/src/canonicalize-url.ts @@ -1,5 +1,5 @@ import * as admin from "firebase-admin"; -import { buildFirebaseImageUrl, parseFirebaseImageUrl } from "./shared-utils"; +import { buildFirebaseImageUrl, parseFirebaseImageUrl } from "../../shared/shared-utils"; export async function canonicalizeUrl(url: string, defaultClassHash: string, firestoreRoot: string) { const { imageClassHash, imageKey } = parseFirebaseImageUrl(url); diff --git a/functions/src/get-image-data.ts b/functions-v1/src/get-image-data.ts similarity index 98% rename from functions/src/get-image-data.ts rename to functions-v1/src/get-image-data.ts index 4db8cc3647..9162ef1ae1 100644 --- a/functions/src/get-image-data.ts +++ b/functions-v1/src/get-image-data.ts @@ -1,6 +1,6 @@ import * as admin from "firebase-admin"; import * as functions from "firebase-functions"; -import { IGetImageDataUnionParams, isWarmUpParams } from "./shared"; +import { IGetImageDataUnionParams, isWarmUpParams } from "../../shared/shared"; import { validateUserContext } from "./user-context"; // update this when deploying updates to this function diff --git a/functions/src/get-network-document.ts b/functions-v1/src/get-network-document.ts similarity index 99% rename from functions/src/get-network-document.ts rename to functions-v1/src/get-network-document.ts index f42e0f7e14..129a0cbb0e 100644 --- a/functions/src/get-network-document.ts +++ b/functions-v1/src/get-network-document.ts @@ -2,7 +2,7 @@ import * as admin from "firebase-admin"; import * as functions from "firebase-functions"; import { canonicalizeUrl } from "./canonicalize-url"; import { parseDocumentContent } from "./parse-document-content"; -import { IGetNetworkDocumentUnionParams, isWarmUpParams } from "./shared"; +import { IGetNetworkDocumentUnionParams, isWarmUpParams } from "../../shared/shared"; import { validateUserContext } from "./user-context"; // update this when deploying updates to this function diff --git a/functions/src/get-network-resources.ts b/functions-v1/src/get-network-resources.ts similarity index 99% rename from functions/src/get-network-resources.ts rename to functions-v1/src/get-network-resources.ts index b6b672e554..7e09e91513 100644 --- a/functions/src/get-network-resources.ts +++ b/functions-v1/src/get-network-resources.ts @@ -3,7 +3,7 @@ import * as functions from "firebase-functions"; import { IGetNetworkResourcesUnionParams, INetworkResourceClassResponse, INetworkResourceOfferingResponse, INetworkResourceTeacherClassResponse, INetworkResourceTeacherOfferingResponse, isWarmUpParams -} from "./shared"; +} from "../../shared/shared"; import { validateUserContext } from "./user-context"; // update this when deploying updates to this function diff --git a/functions/src/index.ts b/functions-v1/src/index.ts similarity index 100% rename from functions/src/index.ts rename to functions-v1/src/index.ts diff --git a/functions/src/parse-document-content.ts b/functions-v1/src/parse-document-content.ts similarity index 97% rename from functions/src/parse-document-content.ts rename to functions-v1/src/parse-document-content.ts index aafecb72fa..872227ac7d 100644 --- a/functions/src/parse-document-content.ts +++ b/functions-v1/src/parse-document-content.ts @@ -1,5 +1,5 @@ -import { IDocumentContent } from "./shared"; -import { matchAll, parseFirebaseImageUrl, replaceAll, safeJsonParse } from "./shared-utils"; +import { IDocumentContent } from "../../shared/shared"; +import { matchAll, parseFirebaseImageUrl, replaceAll, safeJsonParse } from "../../shared/shared-utils"; // regular expression for identifying firebase image urls in document content // In some tile state the image URLS are inside of a double escaped JSON. This means diff --git a/functions/src/portal-types.ts b/functions-v1/src/portal-types.ts similarity index 100% rename from functions/src/portal-types.ts rename to functions-v1/src/portal-types.ts diff --git a/functions/src/post-document-comment.ts b/functions-v1/src/post-document-comment.ts similarity index 98% rename from functions/src/post-document-comment.ts rename to functions-v1/src/post-document-comment.ts index 062b443ea7..d0e4bc2b07 100644 --- a/functions/src/post-document-comment.ts +++ b/functions-v1/src/post-document-comment.ts @@ -2,7 +2,7 @@ import * as admin from "firebase-admin"; import * as functions from "firebase-functions"; import { IPostDocumentCommentUnionParams, isCurriculumMetadata, isDocumentMetadata, isWarmUpParams -} from "./shared"; +} from "../../shared/shared"; import { validateUserContext } from "./user-context"; import { createCommentableDocumentIfNecessary } from "./validate-commentable-document"; diff --git a/functions/src/publish-support.ts b/functions-v1/src/publish-support.ts similarity index 95% rename from functions/src/publish-support.ts rename to functions-v1/src/publish-support.ts index ca4ae5bd93..0215a35ebf 100644 --- a/functions/src/publish-support.ts +++ b/functions-v1/src/publish-support.ts @@ -2,8 +2,8 @@ import * as admin from "firebase-admin"; import * as functions from "firebase-functions"; import { canonicalizeUrl } from "./canonicalize-url"; import { parseDocumentContent } from "./parse-document-content"; -import { IPublishSupportUnionParams, isWarmUpParams } from "./shared"; -import { parseFirebaseImageUrl } from "./shared-utils"; +import { IPublishSupportUnionParams, isWarmUpParams } from "../../shared/shared"; +import { parseFirebaseImageUrl } from "../../shared/shared-utils"; import { validateUserContext } from "./user-context"; // update this when deploying updates to this function diff --git a/functions/src/user-context.ts b/functions-v1/src/user-context.ts similarity index 98% rename from functions/src/user-context.ts rename to functions-v1/src/user-context.ts index 87183f1f9d..02bc8c64b4 100644 --- a/functions/src/user-context.ts +++ b/functions-v1/src/user-context.ts @@ -1,5 +1,5 @@ import { AuthData } from "firebase-functions/lib/common/providers/https"; -import { escapeKey, IUserContext } from "./shared"; +import { escapeKey, IUserContext } from "../../shared/shared"; export interface IValidatedUserContext { isValid: boolean; diff --git a/functions/src/validate-commentable-document.ts b/functions-v1/src/validate-commentable-document.ts similarity index 99% rename from functions/src/validate-commentable-document.ts rename to functions-v1/src/validate-commentable-document.ts index 4c37811061..dcfe48f735 100644 --- a/functions/src/validate-commentable-document.ts +++ b/functions-v1/src/validate-commentable-document.ts @@ -3,7 +3,7 @@ import * as functions from "firebase-functions"; import { ICommentableDocumentParams, ICommentableDocumentUnionParams, isCurriculumMetadata, isDocumentMetadata, isWarmUpParams, networkDocumentKey -} from "./shared"; +} from "../../shared/shared"; import { validateUserContext } from "./user-context"; // update this when deploying updates to this function diff --git a/functions/test/canonicalize-url.test.ts b/functions-v1/test/canonicalize-url.test.ts similarity index 89% rename from functions/test/canonicalize-url.test.ts rename to functions-v1/test/canonicalize-url.test.ts index 427a9a6e9f..95f94b9e87 100644 --- a/functions/test/canonicalize-url.test.ts +++ b/functions-v1/test/canonicalize-url.test.ts @@ -1,5 +1,5 @@ import { canonicalizeUrl } from "../src/canonicalize-url"; -import { buildFirebaseImageUrl } from "../src/shared-utils"; +import { buildFirebaseImageUrl } from "../../shared/shared-utils"; describe("canonicalizeUrl", () => { it("should simply return invalid urls", async () => { diff --git a/functions/test/get-image-data.test.ts b/functions-v1/test/get-image-data.test.ts similarity index 99% rename from functions/test/get-image-data.test.ts rename to functions-v1/test/get-image-data.test.ts index 496d1c7b13..219f674b1b 100644 --- a/functions/test/get-image-data.test.ts +++ b/functions-v1/test/get-image-data.test.ts @@ -1,6 +1,6 @@ import { apps, clearFirestoreData, initializeAdminApp } from "@firebase/rules-unit-testing"; import { getImageData } from "../src/get-image-data"; -import { IGetImageDataParams, IUserContext } from "../src/shared"; +import { IGetImageDataParams, IUserContext } from "../../shared/shared"; import { validateUserContext } from "../src/user-context"; import { configEmulators, diff --git a/functions/test/get-network-document.test.ts b/functions-v1/test/get-network-document.test.ts similarity index 99% rename from functions/test/get-network-document.test.ts rename to functions-v1/test/get-network-document.test.ts index 064b2cd2af..14bacb2e04 100644 --- a/functions/test/get-network-document.test.ts +++ b/functions-v1/test/get-network-document.test.ts @@ -1,8 +1,8 @@ import { apps, clearFirestoreData, initializeAdminApp} from "@firebase/rules-unit-testing"; import { getNetworkDocument } from "../src/get-network-document"; -import { IGetNetworkDocumentParams } from "../src/shared"; -import { buildFirebaseImageUrl, parseFirebaseImageUrl } from "../src/shared-utils"; +import { IGetNetworkDocumentParams } from "../../shared/shared"; +import { buildFirebaseImageUrl, parseFirebaseImageUrl } from "../../shared/shared-utils"; import { validateUserContext } from "../src/user-context"; import { configEmulators, diff --git a/functions/test/get-network-resources.test.ts b/functions-v1/test/get-network-resources.test.ts similarity index 99% rename from functions/test/get-network-resources.test.ts rename to functions-v1/test/get-network-resources.test.ts index 82c6e2e215..474e8daedf 100644 --- a/functions/test/get-network-resources.test.ts +++ b/functions-v1/test/get-network-resources.test.ts @@ -1,7 +1,7 @@ import { apps, clearFirestoreData, initializeAdminApp} from "@firebase/rules-unit-testing"; import { getNetworkResources } from "../src/get-network-resources"; -import { IGetNetworkResourcesParams } from "../src/shared"; +import { IGetNetworkResourcesParams } from "../../shared/shared"; import { configEmulators, kClassHash, kOffering1Id, kOffering2Id, kOtherClassHash, kProblemPath, kTeacherName, kTeacherNetwork, kUserId, specAuth, specUserContext diff --git a/functions/test/parse-document-content.test.ts b/functions-v1/test/parse-document-content.test.ts similarity index 99% rename from functions/test/parse-document-content.test.ts rename to functions-v1/test/parse-document-content.test.ts index 7c2abad953..5a1e6b2db0 100644 --- a/functions/test/parse-document-content.test.ts +++ b/functions-v1/test/parse-document-content.test.ts @@ -1,5 +1,5 @@ import { parseDocumentContent } from "../src/parse-document-content"; -import { buildFirebaseImageUrl, parseFirebaseImageUrl, replaceAll } from "../src/shared-utils"; +import { buildFirebaseImageUrl, parseFirebaseImageUrl, replaceAll } from "../../shared/shared-utils"; import { specDocumentContent } from "./test-utils"; import sharedDatasetExample from "./shared-dataset-example"; diff --git a/functions/test/post-document-comment.test.ts b/functions-v1/test/post-document-comment.test.ts similarity index 99% rename from functions/test/post-document-comment.test.ts rename to functions-v1/test/post-document-comment.test.ts index c1bdd4c008..4de8cc25ed 100644 --- a/functions/test/post-document-comment.test.ts +++ b/functions-v1/test/post-document-comment.test.ts @@ -4,7 +4,7 @@ import { postDocumentComment } from "../src/post-document-comment"; import { ICurriculumMetadata, IDocumentMetadata, IPostDocumentCommentParams, isCurriculumMetadata, IUserContext, networkDocumentKey -} from "../src/shared"; +} from "../../shared/shared"; import { configEmulators, kCanonicalPortal, kCurriculumKey, kDemoName, kDocumentKey, kDocumentType, kFirebaseUserId, kTeacherName, kTeacherNetwork, kUserId, specAuth, specUserContext diff --git a/functions/test/publish-support.test.ts b/functions-v1/test/publish-support.test.ts similarity index 99% rename from functions/test/publish-support.test.ts rename to functions-v1/test/publish-support.test.ts index 21c5e2cd3a..83fd9d79fc 100644 --- a/functions/test/publish-support.test.ts +++ b/functions-v1/test/publish-support.test.ts @@ -1,7 +1,7 @@ import { apps, clearFirestoreData, initializeAdminApp } from "@firebase/rules-unit-testing"; import { publishSupport } from "../src/publish-support"; -import { IPublishSupportParams } from "../src/shared"; -import { buildFirebaseImageUrl, parseFirebaseImageUrl, replaceAll } from "../src/shared-utils"; +import { IPublishSupportParams } from "../../shared/shared"; +import { buildFirebaseImageUrl, parseFirebaseImageUrl, replaceAll } from "../../shared/shared-utils"; import { configEmulators, kCanonicalPortal, kClassHash, kOtherClassHash, kPortal, kTeacherNetwork, diff --git a/functions/test/shared-dataset-example.ts b/functions-v1/test/shared-dataset-example.ts similarity index 100% rename from functions/test/shared-dataset-example.ts rename to functions-v1/test/shared-dataset-example.ts diff --git a/functions/test/test-utils.ts b/functions-v1/test/test-utils.ts similarity index 98% rename from functions/test/test-utils.ts rename to functions-v1/test/test-utils.ts index d5d8c4c84f..d18af8ae7f 100644 --- a/functions/test/test-utils.ts +++ b/functions-v1/test/test-utils.ts @@ -1,7 +1,7 @@ import { useEmulators } from "@firebase/rules-unit-testing"; import { AuthData } from "firebase-functions/lib/common/providers/https"; import { DeepPartial } from "utility-types"; -import { IRowMapEntry, ITileMapEntry, IUserContext } from "../src/shared"; +import { IRowMapEntry, ITileMapEntry, IUserContext } from "../../shared/shared"; // You might need to switch this to "localhost" if 127.0.0.1 doesn't work for you export const kEmulatorHost = "127.0.0.1"; diff --git a/functions/test/user-context.test.ts b/functions-v1/test/user-context.test.ts similarity index 99% rename from functions/test/user-context.test.ts rename to functions-v1/test/user-context.test.ts index 56d881e0f8..44998b6d3f 100644 --- a/functions/test/user-context.test.ts +++ b/functions-v1/test/user-context.test.ts @@ -1,5 +1,5 @@ import { AuthData } from "firebase-functions/lib/common/providers/https"; -import { IUserContext } from "../src/shared"; +import { IUserContext } from "../../shared/shared"; import { getFirebaseClassPath, validateUserContext } from "../src/user-context"; import { kCanonicalPortal, kClassHash, kDemoName, kFirebaseUserId, kOtherCanonicalPortal, kOtherClaimPortal, kOtherClassHash, diff --git a/functions/test/validate-commentable-document.test.ts b/functions-v1/test/validate-commentable-document.test.ts similarity index 99% rename from functions/test/validate-commentable-document.test.ts rename to functions-v1/test/validate-commentable-document.test.ts index 0af37b9c08..22b5fa858a 100644 --- a/functions/test/validate-commentable-document.test.ts +++ b/functions-v1/test/validate-commentable-document.test.ts @@ -3,7 +3,7 @@ import { import { ICommentableDocumentParams, ICurriculumMetadata, IDocumentMetadata, isCurriculumMetadata, IUserContext, networkDocumentKey -} from "../src/shared"; +} from "../../shared/shared"; import { validateCommentableDocument } from "../src/validate-commentable-document"; import { configEmulators, kCanonicalPortal, kCurriculumKey, kDemoName, kDocumentKey, kDocumentType, kFirebaseUserId, diff --git a/functions/tsconfig.json b/functions-v1/tsconfig.json similarity index 91% rename from functions/tsconfig.json rename to functions-v1/tsconfig.json index 43e7e79b5a..08b303e1f1 100644 --- a/functions/tsconfig.json +++ b/functions-v1/tsconfig.json @@ -14,6 +14,6 @@ "compileOnSave": true, "include": [ "src", - "test" - ] + "test", + "../shared" ] } diff --git a/functions/tsconfig.prod.json b/functions-v1/tsconfig.prod.json similarity index 100% rename from functions/tsconfig.prod.json rename to functions-v1/tsconfig.prod.json diff --git a/functions-v2/.eslintrc.js b/functions-v2/.eslintrc.js new file mode 100644 index 0000000000..910cb16d6b --- /dev/null +++ b/functions-v2/.eslintrc.js @@ -0,0 +1,37 @@ +module.exports = { + root: true, + env: { + es6: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings", + "plugin:import/typescript", + "google", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + project: ["tsconfig.json", "tsconfig.dev.json"], + tsconfigRootDir: __dirname, + sourceType: "module", + }, + ignorePatterns: [ + "/lib/**/*", // Ignore built files. + "/generated/**/*", // Ignore generated files. + "/jest.config.js", + ], + plugins: [ + "@typescript-eslint", + "import", + ], + rules: { + "quotes": ["error", "double"], + "import/no-unresolved": 0, + "indent": ["error", 2], + "max-len": ["warn", {code: 120, ignoreUrls: true}], + "require-jsdoc": 0, + }, +}; diff --git a/functions-v2/.gitignore b/functions-v2/.gitignore new file mode 100644 index 0000000000..8032f8fe45 --- /dev/null +++ b/functions-v2/.gitignore @@ -0,0 +1,10 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +*.local diff --git a/functions-v2/README.md b/functions-v2/README.md new file mode 100644 index 0000000000..88cac57243 --- /dev/null +++ b/functions-v2/README.md @@ -0,0 +1,50 @@ +# Firebase functions +The functions are split into two folders `functions-v1` and `functions-v2`. This folder `functions-v2` contains the newer functions. We are hoping to incrementally migrate the legacy functions from `functions-v1` into this folder. + +## Available Functions + +|Function|Purpose| +|--------|-------| +|_updateClassDocNetworksOnUserChange_|Monitors Firestore user documents for changes and updates the Firestore class documents with the networks of all of the teachers in these classes| + +Here are the basic development operations you can do after you cd into the `functions-v2` directory: +``` +$ cd functions-v2 +$ npm install # install local dependencies +$ npm run lint # lint the functions code +$ npm run test # runs jest (unit) tests for the functions code (requires emulator, see below) +$ npm run build # build the functions code (transpile TypeScript) +``` + +## Testing cloud functions + +### Running tests locally (without running functions in the emulator) +``` +$ npm run test:emulator # start the firestore and database emulators +$ npm run test # run all tests in `functions` directory +``` +In this approach the functions are running inside of Jest and they connect to the emulated Firestore and Realtime database services. + +The tests use `firebase-functions-test`. This package does a little setup of environment variables so when the functions run they will connect to the emulator. This package also provides a way to mock some standard events and wraps the calls to the functions to emulate how they would be called in the cloud. This is a simple and efficient way of testing the basic functionality without loading the function code into the emulator itself. The downside is that the functions are not responding to real events in Firestore or realtime database. If they are http functions they are not receiving the actual request event. + +#### Notes +In the tests, the function cannot be imported normally. This is because the `firebase-functions-test`'s initialize function has to be called before the function code calls `initializeApp`. The standard practice for Firebase functions seems to be calling `initializeApp` at the module level not inside of the function body, so it will be called when the module is imported. The work around is to dynamically import the function. The docs for the `firebase-functions-test` use `require` to import the function, but we are trying to stick with the `import` syntax. The dynamic `import` syntax is asynchronous so it requires waiting, which means it can't be at the top level of the module. So the dynamic import of the function is inside of the test body. Typescript is able to track down the types for these dynamic imports. There is info about this approach in the code. + +Because the tested functions are not responding to actual changes in the databases, it is necessary for the test to construct an event object that is then passed to the wrapped function. Additionally the database needs to be setup with documents before the test. The test has to make sure the event object is in sync with what is in the database. + +`npm run emulator` and `npm run test:emulator` use a project name of `demo-test`. The `demo-` prefix is special and tells the emulator not to allow connections outside of itself. Without this project name being specified the emulator will use the project defined in `.firebaserc`, and will connect to the real version of any service that isn't being emulated. + +### Running the functions in the emulator +``` +npm run build +npm run emulator +``` +This will load the built function code into the emulator. The only function we have so far is one that monitors Firestore docs for changes. So with the function running in the emulator you can manually change some docs and see if the function responds correctly. + +## To deploy firebase functions to production: +``` +$ npm run deploy # deploy all functions +``` + +## Differences with functions-v1 +- in `v2` the firebase-tools are a devDependency: it is not necessary to install them globally diff --git a/functions-v2/jest.config.js b/functions-v2/jest.config.js new file mode 100644 index 0000000000..879629c095 --- /dev/null +++ b/functions-v2/jest.config.js @@ -0,0 +1,26 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['lib/', 'node_modules/'], + moduleNameMapper: { + // These are necessary so code imported from ../shared/ will use the same version of + // firebase-admin that the local code does. + // The explicit `^` and `$` are needed so this only matches what we are importing. + // Otherwise it breaks the internal firebase admin code's imports + "^firebase-admin$": "/node_modules/firebase-admin", + "^firebase-admin/firestore$": "/node_modules/firebase-admin/lib/firestore", + "^firebase-admin/app$": "/node_modules/firebase-admin/lib/app", + "^firebase-admin/database$": "/node_modules/firebase-admin/lib/database", + }, + // The tests can't be run in parallel because they are using a shared Firestore and + // Realtime database. + maxWorkers: 1, +}; + +// This is configured here because the clearFirebaseData function from +// firebase-functions-test/lib/providers/firestore needs it set +// before the module is imported. +// The port here should match the port that is set in the emulators +// section of firebase.json +process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088"; +process.env["FIREBASE_DATABASE_EMULATOR_HOST"]="127.0.0.1:9000"; diff --git a/functions-v2/package-lock.json b/functions-v2/package-lock.json new file mode 100644 index 0000000000..b7fc085d58 --- /dev/null +++ b/functions-v2/package-lock.json @@ -0,0 +1,12835 @@ +{ + "name": "functions-v2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions-v2", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.1.1" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "eslint": "^8.22.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-import": "^2.25.4", + "firebase-functions-test": "^3.1.0", + "firebase-tools": "^13.15.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.4", + "typescript": "^4.9.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dev": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/component": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz", + "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==", + "dependencies": { + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz", + "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz", + "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/database": "1.0.7", + "@firebase/database-types": "1.0.4", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz", + "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.7" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz", + "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/cloud-sql-connector": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.3.4.tgz", + "integrity": "sha512-Lw05ME/W9mDzJuQwGwzHl9dqJtN1zBpyK6A3NbjhBi/V1WZpsIk1RgqR6+5LIbMurcNRia9ITOlCjsgJY+H92A==", + "dev": true, + "dependencies": { + "@googleapis/sqladmin": "^19.0.0", + "gaxios": "^6.1.1", + "google-auth-library": "^9.2.0", + "p-throttle": "^5.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.9.0.tgz", + "integrity": "sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/precise-date": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz", + "integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "devOptional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.5.0.tgz", + "integrity": "sha512-ptRLLDrAp1rStD1n3ZrG8FdAfpccqI6M5rCaceF6PL7DU3hqJbvQ2Y91G8MKG7c7zK+jiWv655Qf5r2IvjTzwA==", + "dev": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/precise-date": "^4.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "@opentelemetry/api": "~1.8.0", + "@opentelemetry/semantic-conventions": "~1.21.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.3.0", + "google-gax": "^4.3.3", + "heap-js": "^2.2.0", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.12.1.tgz", + "integrity": "sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@googleapis/sqladmin": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-19.0.0.tgz", + "integrity": "sha512-65zgEpQLhpTZqUic+pm4BbdDByN9NsHkphfCIwzpx3fccHPc6OuKsW0XexYCq9oTUtTC4QRjFisBDLV9fChRtg==", + "dev": true, + "dependencies": { + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", + "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "devOptional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "devOptional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", + "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "devOptional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "devOptional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", + "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", + "dependencies": { + "undici-types": "~6.18.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "devOptional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "devOptional": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "optional": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha512-1Sd1LrodN0XYxYeZcN1J4xYZvmvTwD5tDWaPUGPIzH1mFsmzsPnVtd2exWhecMjtZk/wYWjNZJiD3b1SLCeJqg==", + "dev": true + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "devOptional": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha512-kiV+/DTgVro4aZifY/hwRwALBISViL5NP4aReaR2EVJEObpbUBHIkdJh/YpcoEiYt7nBodZ6U2ajZeZvSxUCCg==", + "dev": true + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "optional": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "optional": true + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha512-yKNcXi/Mvi5kb1uK0sahubYiyfUO2EUgOp4NcY9+8NX5Xmc+4yeNogZuLFkpLBBj7/QI9MjRUIuXrV9XOw5kVg==", + "dev": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "dev": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/configstore/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/configstore/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-env": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", + "integrity": "sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.5" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-env/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cross-env/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/cross-env/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cross-env/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cross-env/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/csv-parse": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", + "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==", + "dev": true + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal-in-any-order": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", + "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", + "dev": true, + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.7.tgz", + "integrity": "sha512-6FTNWIWMxMy/ZY6799nBlPtF1DFDQ6VQJ7yyDP27SJNt5lwtQ5ufqVvHylb3fdQefvRcgA3fKcFMJi9OLwBRNw==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "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==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "optional": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exegesis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.2.0.tgz", + "integrity": "sha512-MOzRyqhvl+hTA4+W4p0saWRIPlu0grIx4ykjMEYgGLiqr/z9NCIlwSq2jF0gyxNjPZD3xyHgmkW6BSaLVUdctg==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^10.3.10", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^3.1.1", + "promise-breaker": "^6.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "dev": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/exegesis/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/exegesis/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/exegesis/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/exegesis/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "optional": true + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, + "node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "dev": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz", + "integrity": "sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-admin": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.3.1.tgz", + "integrity": "sha512-vEr3s3esl8nPIA9r/feDT4nzIXCfov1CyyCSpMQWp6x63Q104qke0MEGZlrHUZVROtl8FLus6niP/M9I1s4VBA==", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.3.0.tgz", + "integrity": "sha512-X+OOA34MGrsTimFXTDnWT0psAqnmBkJ85bGCoLMwjgei5Prfkqh3bv5QASnXC/cmIVBSF2Qw9uW1+mF/t3kFlw==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/firebase-tools": { + "version": "13.15.1", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-13.15.1.tgz", + "integrity": "sha512-HD92RbtG3uVeC8KgiIC6ZlQjr5ep6g4Utcv23QOr2sBfwZ1UDQW9Kg89HKPYgEC2Y7h7ANIF+TZqDOvDIA1n5A==", + "dev": true, + "dependencies": { + "@google-cloud/cloud-sql-connector": "^1.3.3", + "@google-cloud/pubsub": "^4.5.0", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^7.0.0", + "async-lock": "1.4.1", + "body-parser": "^1.19.0", + "chokidar": "^3.6.0", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.2.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.7.0", + "glob": "^10.4.1", + "google-auth-library": "^9.11.0", + "inquirer": "^8.2.6", + "inquirer-autocomplete-prompt": "^2.0.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^13.0.2", + "marked-terminal": "^7.0.0", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "pg": "^8.11.3", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^6.3.0", + "retry": "^0.13.1", + "rimraf": "^5.0.0", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.3", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3", + "yaml": "^2.4.1" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": ">=18.0.0 || >=20.0.0" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "devOptional": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "devOptional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha512-ZwFh34WZhZX28ntCMAP1mwyAJkn8+Omagvt/GvA+JQM/qgT0+MR2NPF3vhvgdshfdvDyGZXs8fPXW84K32Wjuw==", + "dev": true + }, + "node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha512-5MUzqFiycIKLMD1B0dYOE4hGgLLUZUNGGYO4BExdwT32wUwW3DBOE7lMQars7vB1q43Fb3Tyt+HmgLKsJhDYdg==", + "dev": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "devOptional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.9.tgz", + "integrity": "sha512-tcjQr7sXVGMdlvcG25wSv98ap1dtF4Z6mcV0rztGIddOcezw4YMb/uTXg72JPrLep+kXcVjaJjg6oo3KLf4itQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "devOptional": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "devOptional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/heap-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.5.0.tgz", + "integrity": "sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "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==", + "dev": true, + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "devOptional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-2.0.1.tgz", + "integrity": "sha512-jUHrH0btO7j5r8DTQgANf2CBkTZChoVySD8zF/wp5fZCOLIuUbleXhf4ZY5jNBOc1owA3gdfWtfZuppfYBhcUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.2", + "figures": "^3.2.0", + "picocolors": "^1.0.0", + "run-async": "^2.4.1", + "rxjs": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "inquirer": "^8.0.0" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/install-artifact-from-github": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz", + "integrity": "sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg==", + "dev": true, + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "optional": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "dev": true + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "node_modules/is2": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", + "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha512-jnt9OC34sLXMLJ6YfPQ2ZEKrR9mB5ZbSnQb4LPaOx1c5rTzxpR33L18jjp0r75mGGTJmsil3qwN1B5IBeTnSSA==", + "dev": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "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==", + "dev": true + }, + "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", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", + "dev": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/json-ptr": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz", + "integrity": "sha512-SiSJQ805W1sDUCD1+/t1/1BIrveq2Fe9HJqENxZmMCILmrPI7WhS/pePpIOx85v6/H2z1Vy7AI08GV2TzfXocg==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libsodium": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", + "dev": true + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", + "dev": true, + "dependencies": { + "libsodium": "^0.7.15" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha512-XpqGh1e7hhkOzftBfWE7zt+Yn9mVHFkDhicVttvKLsoCMLVVL+xTQjfjB4X4vtznauxv0QZ5ZAeqjvat0dh62Q==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "devOptional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha512-sTebg2a1PoicYEZXD5PBdQcTlIJ6hUslrlWr7iV0O7n+i4596s2NQ9I5CaZ5FbXSfya/9WQsrYLANUJv9paYVA==", + "dev": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dev": true, + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "optional": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-terminal": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz", + "integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "chalk": "^5.3.0", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.1.3", + "supports-hyperlinks": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <14" + } + }, + "node_modules/marked-terminal/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "dev": true, + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/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==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "dev": true, + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "optional": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openapi3-ts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", + "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", + "dev": true, + "dependencies": { + "yaml": "^2.2.1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-throttle": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-5.1.0.tgz", + "integrity": "sha512-+N+s2g01w1Zch4D0K3OpnPDqLOKmLcQ4BvIFq3JC0K29R28vUOjWpO+OJZBNt8X9i3pFCksZJZ0YXkUGjaFE6g==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dev": true, + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "optional": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-breaker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true + }, + "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" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/re2": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.21.3.tgz", + "integrity": "sha512-GI+KoGkHT4kxTaX+9p0FgNB1XUnCndO9slG5qqeEoZ7kbf6Dk6ohQVpmwKVeSp7LPLn+g6Q3BaCopz4oHuBDuQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.5", + "nan": "^2.20.0", + "node-gyp": "^10.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "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==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "devOptional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.8.tgz", + "integrity": "sha512-461UFH44NtSfIlS83PUg2N7OZo86BC/kB3dY77gJdsODsBhhw7+2uE0tzTINxrY9CahCUVk1VhpWCA5i1yoIEg==", + "dev": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "dev": true + }, + "node_modules/router/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/sql-formatter": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.0.tgz", + "integrity": "sha512-h3uVulRmOfARvDejuSzs9GMbua/UmGCKiP08zyHT1PnG376zk9CHVsDAcKIc9TcIwIrDH3YULWwI4PrXdmLRVw==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "dev": true + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "devOptional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-json": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.8.0.tgz", + "integrity": "sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==", + "dev": true, + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "devOptional": true + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "devOptional": true + }, + "node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "dev": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/superstatic/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "dev": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "devOptional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "devOptional": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "devOptional": true + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true + }, + "node_modules/ts-jest": { + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", + "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", + "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "optional": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/universal-analytics/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "dev": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw==", + "dev": true + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "devOptional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "dev": true, + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.6.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "dev": true, + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "devOptional": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + } + } +} diff --git a/functions-v2/package.json b/functions-v2/package.json new file mode 100644 index 0000000000..3ccecef6e9 --- /dev/null +++ b/functions-v2/package.json @@ -0,0 +1,40 @@ +{ + "name": "functions-v2", + "scripts": { + "lint": "eslint --ext .js,.ts .", + "build": "tsc", + "build:watch": "tsc --watch", + "emulator": "firebase emulators:start --project demo-test", + "emulator:online": "firebase emulators:start", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell --project demo-test", + "start": "npm run shell", + "test": "jest", + "test:emulator": "firebase emulators:start --project demo-test --only firestore,database", + "deploy": "firebase deploy --only functions:functions-v2", + "logs": "firebase functions:log" + }, + "engines": { + "node": "20" + }, + "main": "lib/functions-v2/src/index.js", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.1.1" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "eslint": "^8.22.0", + "eslint-config-google": "^0.14.0", + "eslint-plugin-import": "^2.25.4", + "firebase-functions-test": "^3.1.0", + "firebase-tools": "^13.15.1", + "jest": "^29.7.0", + "ts-jest": "^29.2.4", + "typescript": "^4.9.0" + }, + "private": true +} diff --git a/functions-v2/src/at-midnight.ts b/functions-v2/src/at-midnight.ts new file mode 100644 index 0000000000..e4c8cef364 --- /dev/null +++ b/functions-v2/src/at-midnight.ts @@ -0,0 +1,28 @@ +import {onSchedule} from "firebase-functions/v2/scheduler"; +import * as logger from "firebase-functions/logger"; + +// NOTE: in order for this import from shared to work it is necessary +// to alias "firebase-admin" in tsconfig.json. Otherwise Typescript will +// read the types from the parent node_modules. The parent directory +// has a different version of the firebase dependencies, which cause +// type errors. +import {cleanFirebaseRoots} from "../../shared/clean-firebase-roots"; + +export const atMidnight = onSchedule("0 7 * * *", runAtMidnight); + +// This function is split out so it can be tested by Jest. The +// firebase-functions-test library doesn't support wrapping onSchedule. +export async function runAtMidnight() { + await cleanFirebaseRoots({ + appMode: "qa", + hoursAgo: 24, + logger, + dryRun: false, + }); + + // When cleanFirebaseRoots is called from a NodeJS script it is + // necessary to call Firebase's deleteApp so no threads are left running. + // Inside of a firebase function according to + // https://stackoverflow.com/a/72933644/3195497 + // it isn't necessary to call deleteApp when the function is done. +} diff --git a/functions-v2/src/index.ts b/functions-v2/src/index.ts new file mode 100644 index 0000000000..94d32083d1 --- /dev/null +++ b/functions-v2/src/index.ts @@ -0,0 +1,5 @@ +import * as admin from "firebase-admin"; +export {onUserDocWritten} from "./on-user-doc-written"; +export {atMidnight} from "./at-midnight"; + +admin.initializeApp(); diff --git a/functions-v2/src/on-user-doc-written.ts b/functions-v2/src/on-user-doc-written.ts new file mode 100644 index 0000000000..f94fe5d527 --- /dev/null +++ b/functions-v2/src/on-user-doc-written.ts @@ -0,0 +1,79 @@ +import {onDocumentWritten} from "firebase-functions/v2/firestore"; +import * as logger from "firebase-functions/logger"; +import * as admin from "firebase-admin"; +import {isArrayEqual} from "./utils"; + +export const onUserDocWritten = + onDocumentWritten("{root}/{space}/users/{userId}", async (event) => { + const {root, space, userId} = event.params; + + const classesResult = await admin.firestore() + .collection(`${root}/${space}/classes`) + .where("teachers", "array-contains", userId) + .get(); + + // For every class of this teacher update the networks. + // We could do something more efficient in the case where a network was + // added. Or in the case that networks were not changed. + // That can be figured out by looking at the event.data.before and + // event.data.after documents. + // However to keep the code more simple we just always do the scan + // of classes and teachers. This is required when a network is deleted + // because we need to figure out if another teacher in the class still has + // the deleted network. + + // To optimize this we collect all of the teachers we care about + // and make one request for them instead of requesting the teachers for each + // class separately. + + const teacherIdSet = new Set(); + classesResult.forEach((classDoc) => { + const {teachers} = classDoc.data() as {teachers: string[]}; + if (!Array.isArray(teachers)) return; + teachers.forEach((id) => teacherIdSet.add(id)); + }); + + const teacherIds = [...teacherIdSet]; + + const teacherNetworks: Record = {}; + + // Need to use batching incase the number of teacherIds is larger than 30 + const batchSize = 30; + for (let i = 0; i < teacherIds.length; i += batchSize) { + const batch = teacherIds.slice(i, i + batchSize); + const teachersResult = await admin.firestore() + .collection(`${root}/${space}/users`) + .where("uid", "in", batch) + .get(); + + teachersResult.forEach((teacherDoc) => { + const teacherData = teacherDoc.data(); + teacherNetworks[teacherData.uid] = teacherData.networks; + }); + } + + const classUpdatePromises: Promise[] = []; + classesResult.forEach((classDoc) => { + // Update each class with the networks of each teacher in the class + const {teachers, networks} = classDoc.data() as {teachers: string[], networks: string[] | undefined}; + if (!Array.isArray(teachers)) return; + const classNetworks = new Set(); + teachers.forEach((teacher) => { + const networks = teacherNetworks[teacher]; + if (!networks) return; + networks.forEach((network) => classNetworks.add(network)); + }); + const orderedNetworks = [...classNetworks].sort(); + if (isArrayEqual(networks, orderedNetworks)) return; + + classUpdatePromises.push( + classDoc.ref.update({networks: orderedNetworks}) + ); + }); + + await Promise.all(classUpdatePromises); + + logger.info("User updated", event.document); + }); + + diff --git a/functions-v2/src/utils.ts b/functions-v2/src/utils.ts new file mode 100644 index 0000000000..118c4acc63 --- /dev/null +++ b/functions-v2/src/utils.ts @@ -0,0 +1,3 @@ +export function isArrayEqual(array1: string[] | undefined, array2: string[]) { + return array1?.length === array2.length && array1.every((value, index) => value === array2[index]); +} diff --git a/functions-v2/test/at-midnight.test.ts b/functions-v2/test/at-midnight.test.ts new file mode 100644 index 0000000000..9639eadafe --- /dev/null +++ b/functions-v2/test/at-midnight.test.ts @@ -0,0 +1,94 @@ +import { + clearFirestoreData, +} from "firebase-functions-test/lib/providers/firestore"; +import {getFirestore, Timestamp} from "firebase-admin/firestore"; +import {getDatabase} from "firebase-admin/database"; +import * as logger from "firebase-functions/logger"; +import {initialize, projectConfig} from "./initialize"; +import {runAtMidnight} from "../src/at-midnight"; + +jest.mock("firebase-functions/logger"); + +const {cleanup} = initialize(); + +const HOUR = 1000 * 60 * 60; + +async function writeFirestoreRoot(lastLaunchMillis = 0) { + const newRoot = getFirestore() + .collection("qa") + .doc(); + + await newRoot.set({ + lastLaunchTime: Timestamp.fromMillis(lastLaunchMillis), + }); + + // Add some sub docs to make sure they are deleted + await newRoot.collection("users").doc().set({ + uid: "test-user", + }); + + return newRoot; +} + +async function writeDatabaseRoot(rootId: string) { + getDatabase().ref("qa").child(rootId).set({someField: "firebase realtime database"}); +} + +// In other tests we use firebase-functions-test to wrap the function. +// In this case it would look like: +// const wrapped = fft.wrap(atMidnight); +// However the wrapper doesn't support onSchedule: +// - The Typescript types don't allow it +// - at run time it doesn't pass the right event: +// https://github.com/firebase/firebase-functions-test/issues/210 +// So instead the code is separated from the onSchedule and called directly. + +describe("atMidnight", () => { + beforeEach(async () => { + await clearFirestoreData(projectConfig); + await getDatabase().ref().set(null); + }); + + test("clean up firestore roots with no database roots", async () => { + await writeFirestoreRoot(); + await runAtMidnight(); + + const roots = await getFirestore().collection("qa").get(); + expect(roots.size).toBe(0); + expect(logger.info) + .toHaveBeenCalledWith("Found 1 roots to delete"); + }); + + test("clean up firestore root and database root", async () => { + const firestoreRoot = await writeFirestoreRoot(); + await writeDatabaseRoot(firestoreRoot.id); + + await runAtMidnight(); + + const fsRoots = await getFirestore().collection("qa").get(); + expect(fsRoots.size).toBe(0); + const dbRoots = await getDatabase().ref("qa").get(); + expect(dbRoots.val()).toEqual(null); + expect(logger.info) + .toHaveBeenCalledWith("Found 1 roots to delete"); + }); + + test("only clean up firestore roots older than 24 hours", async () => { + await writeFirestoreRoot(Date.now() - HOUR); + await writeFirestoreRoot(Date.now() - 12*HOUR); + await writeFirestoreRoot(Date.now() - 23*HOUR); + await writeFirestoreRoot(Date.now() - 25*HOUR); + await writeFirestoreRoot(Date.now() - 48*HOUR); + + await runAtMidnight(); + + const roots = await getFirestore().collection("qa").get(); + expect(roots.size).toBe(3); + expect(logger.info) + .toHaveBeenCalledWith("Found 2 roots to delete"); + }); + + afterAll(async () => { + await cleanup(); + }); +}); diff --git a/functions-v2/test/initialize.ts b/functions-v2/test/initialize.ts new file mode 100644 index 0000000000..61497f4429 --- /dev/null +++ b/functions-v2/test/initialize.ts @@ -0,0 +1,30 @@ +import {deleteApp, initializeApp} from "firebase-admin/app"; +import initializeFFT from "firebase-functions-test"; + +export const projectConfig = { + projectId: "demo-test", + // This URL doesn't have to be valid, it just has to a non empty string + // The actual database host will be picked up from + // FIREBASE_DATABASE_EMULATOR_HOST + // This is defined in jest.config.js + databaseURL: "https://not-a-project.firebaseio.com", +}; + +export function initialize() { + const fft = initializeFFT(projectConfig); + + // When the function is running in the cloud initializeApp is called by index.ts + // In our tests we import the function's module directly so we can call + // initializeApp ourselves. This is beneficial since initializeApp needs to + // be called after initializeFFT above. + const fbApp = initializeApp(); + + const cleanup = async () => { + fft.cleanup(); + // Deleting the Firebase app is necessary for the Jest tests to exit when they + // are complete. FFT creates a testApp which it deletes in cleanup(), but + // we are not using this testApp. + await deleteApp(fbApp); + }; + return {fft, fbApp, cleanup}; +} diff --git a/functions-v2/test/on-user-doc-written.test.ts b/functions-v2/test/on-user-doc-written.test.ts new file mode 100644 index 0000000000..9bcc865cf5 --- /dev/null +++ b/functions-v2/test/on-user-doc-written.test.ts @@ -0,0 +1,286 @@ +import { + clearFirestoreData, +} from "firebase-functions-test/lib/providers/firestore"; +import * as logger from "firebase-functions/logger"; +import * as admin from "firebase-admin"; +import {initialize, projectConfig} from "./initialize"; +import {onUserDocWritten} from "../src/on-user-doc-written"; + +jest.mock("firebase-functions/logger"); + +const {fft, cleanup} = initialize(); + +type CollectionRef = admin.firestore.CollectionReference< + admin.firestore.DocumentData, admin.firestore.DocumentData +>; + +describe("functions", () => { + beforeEach(async () => { + await clearFirestoreData(projectConfig); + }); + + describe("onUserDocWritten", () => { + let classesCollection: CollectionRef; + let usersCollection: CollectionRef; + + beforeEach(() => { + classesCollection = admin.firestore().collection("demo/test/classes"); + usersCollection = admin.firestore().collection("demo/test/users"); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function writeClassDocs(classDocs: any[]) { + return Promise.all(classDocs.map((classDoc) => { + return classesCollection + .doc(classDoc.context_id) + .set(classDoc); + })); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function writeUserDocs(userDocs: any[]) { + return Promise.all(userDocs.map((userDoc) => { + return usersCollection + .doc(userDoc.uid) + .set(userDoc); + })); + } + + test("add new network", async () => { + const wrapped = fft.wrap(onUserDocWritten); + + const event = { + params: { + root: "demo", + space: "test", + userId: "1234", + }, + }; + + await writeClassDocs([ + { + context_id: "testclass-1", + id: "1", + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + + await writeUserDocs([ + { + uid: "1236", + type: "teacher", + networks: ["other-network"], + }, + { + uid: "1234", + type: "teacher", + networks: ["test-network"], + }, + ]); + + await wrapped(event); + + expect(logger.info) + .toHaveBeenCalledWith("User updated", "demo/test/users/1234" ); + + const classDocsResult = await classesCollection.get(); + const classDocs = classDocsResult.docs.map((doc) => doc.data()); + expect(classDocs).toEqual([ + { + context_id: "testclass-1", + id: "1", + networks: ["test-network"], + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network", "test-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + }); + + test("remove network", async () => { + const wrapped = fft.wrap(onUserDocWritten); + + const event = { + params: { + root: "demo", + space: "test", + userId: "1234", + }, + }; + + await writeClassDocs([ + { + context_id: "testclass-1", + id: "1", + networks: ["test-network"], + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network", "test-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + + await writeUserDocs([ + { + uid: "1234", + type: "teacher", + }, + { + uid: "1236", + type: "teacher", + networks: ["other-network"], + }, + ]); + + await wrapped(event); + + expect(logger.info) + .toHaveBeenCalledWith("User updated", "demo/test/users/1234" ); + + const classDocsResult = await classesCollection.get(); + const classDocs = classDocsResult.docs.map((doc) => doc.data()); + expect(classDocs).toEqual([ + { + context_id: "testclass-1", + id: "1", + networks: [], + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + }); + + // If there is overlap between the networks of the co-teachers then removing + // a network from one co-teacher might not change the networks of the class + test("no network change in a class", async () => { + const wrapped = fft.wrap(onUserDocWritten); + + const event = { + params: { + root: "demo", + space: "test", + userId: "1234", + }, + }; + + await writeClassDocs([ + { + context_id: "testclass-1", + id: "1", + networks: ["test-network"], + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network", "test-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + + await writeUserDocs([ + { + uid: "1234", + type: "teacher", + }, + { + uid: "1236", + type: "teacher", + networks: ["other-network", "test-network"], + }, + ]); + + await wrapped(event); + + expect(logger.info) + .toHaveBeenCalledWith("User updated", "demo/test/users/1234" ); + + const classDocsResult = await classesCollection.get(); + const classDocs = classDocsResult.docs.map((doc) => doc.data()); + expect(classDocs).toEqual([ + { + context_id: "testclass-1", + id: "1", + networks: [], + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }, + { + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }, + { + context_id: "testclass-3", + id: "2", + networks: ["other-network", "test-network"], + teachers: ["1234", "1236"], + uri: "https://example.concord.org/classes/2", + }, + ]); + }); + }); + + afterAll(async () => { + await cleanup(); + }); +}); diff --git a/functions-v2/tsconfig.dev.json b/functions-v2/tsconfig.dev.json new file mode 100644 index 0000000000..7560eed4ca --- /dev/null +++ b/functions-v2/tsconfig.dev.json @@ -0,0 +1,5 @@ +{ + "include": [ + ".eslintrc.js" + ] +} diff --git a/functions-v2/tsconfig.json b/functions-v2/tsconfig.json new file mode 100644 index 0000000000..c3db751f1f --- /dev/null +++ b/functions-v2/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + // This prevents typescript from trying to include @types from the parent folders. + // The types in the parent folders conflict so they break the build. + "typeRoots": ["./node_modules/@types"], + "paths": { + // These are necessary so code imported from ../shared/ will use the same version of + // firebase-admin that the local code does. Technically only "firebase-admin/firestore" + // seems to be currently required, but it seems safer to alias all of the admin + // libraries the shared code might be using. + "firebase-admin": ["./node_modules/firebase-admin/lib"], + "firebase-admin/firestore": ["./node_modules/firebase-admin/lib/firestore"], + "firebase-admin/app": ["./node_modules/firebase-admin/lib/app"], + "firebase-admin/database": ["./node_modules/firebase-admin/lib/database"], + }, + + }, + "compileOnSave": true, + "include": [ + "src", + "test" + ], + "exclude": ["**/node_modules", "**/.*/"] +} diff --git a/package-lock.json b/package-lock.json index f574f52cd9..f02347bd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,12 @@ "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", "@floating-ui/react": "^0.25.4", + "@visx/axis": "3.10.1", + "@visx/grid": "3.5.0", + "@visx/group": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "@visx/text": "3.3.0", "chart.js": "^2.9.4", "classnames": "^2.3.1", "client-oauth2": "^4.3.3", @@ -6258,8 +6264,9 @@ "license": "MIT" }, "node_modules/@types/d3-geo": { - "version": "3.0.3", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "dependencies": { "@types/geojson": "*" } @@ -7425,6 +7432,233 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@visx/axis": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.10.1.tgz", + "integrity": "sha512-HBEDLcpZoJ16hFbkYu3S6mN5mbwlFmUWY5yN967X06RdIL4LmAG3gnZ7u4F9buA3LQo+trJXW78moN005odD4Q==", + "dependencies": { + "@types/react": "*", + "@visx/group": "3.3.0", + "@visx/point": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "@visx/text": "3.3.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/curve": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.3.0.tgz", + "integrity": "sha512-G1l1rzGWwIs8ka3mBhO/gj8uYK6XdU/3bwRSoiZ+MockMahQFPog0bUkuVgPwwzPSJfsA/E5u53Y/DNesnHQxg==", + "dependencies": { + "@types/d3-shape": "^1.3.1", + "d3-shape": "^1.0.6" + } + }, + "node_modules/@visx/curve/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "node_modules/@visx/curve/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "dependencies": { + "@types/d3-path": "^1" + } + }, + "node_modules/@visx/curve/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/@visx/curve/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@visx/grid": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-3.5.0.tgz", + "integrity": "sha512-i1pdobTE223ItMiER3q4ojIaZWja3vg46TkS6FotnBZ4c0VRDHSrALQPdi0na+YEgppASWCQ2WrI/vD6mIkhSg==", + "dependencies": { + "@types/react": "*", + "@visx/curve": "3.3.0", + "@visx/group": "3.3.0", + "@visx/point": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.3.0.tgz", + "integrity": "sha512-yKepDKwJqlzvnvPS0yDuW13XNrYJE4xzT6xM7J++441nu6IybWWwextyap8ey+kU651cYDb+q1Oi6aHvQwyEyw==", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.3.0.tgz", + "integrity": "sha512-03eBBIJarkmX79WbeEGTUZwmS5/MUuabbiM9KfkGS9pETBTWkp1DZtEHZdp5z34x5TDQVLSi0rk1Plg3/8RtDg==" + }, + "node_modules/@visx/scale": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.5.0.tgz", + "integrity": "sha512-xo3zrXV2IZxrMq9Y9RUVJUpd93h3NO/r/y3GVi5F9AsbOzOhsLIbsPkunhO9mpUSR8LZ9TiumLEBrY+3frRBSg==", + "dependencies": { + "@visx/vendor": "3.5.0" + } + }, + "node_modules/@visx/shape": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.5.0.tgz", + "integrity": "sha512-DP3t9jBQ7dSE3e6ptA1xO4QAIGxO55GrY/6P+S6YREuQGjZgq20TLYLAsiaoPEzFSS4tp0m12ZTPivWhU2VBTw==", + "dependencies": { + "@types/d3-path": "^1.0.8", + "@types/d3-shape": "^1.3.1", + "@types/lodash": "^4.14.172", + "@types/react": "*", + "@visx/curve": "3.3.0", + "@visx/group": "3.3.0", + "@visx/scale": "3.5.0", + "classnames": "^2.3.1", + "d3-path": "^1.0.5", + "d3-shape": "^1.2.0", + "lodash": "^4.17.21", + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/shape/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "node_modules/@visx/shape/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "dependencies": { + "@types/d3-path": "^1" + } + }, + "node_modules/@visx/shape/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/@visx/shape/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@visx/text": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.3.0.tgz", + "integrity": "sha512-fOimcsf0GtQE9whM5MdA/xIkHMaV29z7qNqNXysUDE8znSMKsN+ott7kSg2ljAEE89CQo3WKHkPNettoVsa84w==", + "dependencies": { + "@types/lodash": "^4.14.172", + "@types/react": "*", + "classnames": "^2.3.1", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "reduce-css-calc": "^1.3.0" + }, + "peerDependencies": { + "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.5.0.tgz", + "integrity": "sha512-yt3SEZRVmt36+APsCISSO9eSOtzQkBjt+QRxNRzcTWuzwMAaF3PHCCSe31++kkpgY9yFoF+Gfes1TBe5NlETiQ==", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-scale": "4.0.2", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-scale": "4.0.2", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@visx/vendor/node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@visx/vendor/node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "dev": true, @@ -17349,6 +17583,11 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, + "node_modules/math-expression-evaluator": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", + "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" + }, "node_modules/mathjs": { "version": "10.6.4", "license": "Apache-2.0", @@ -19687,6 +19926,29 @@ "node": ">=8" } }, + "node_modules/reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", + "dependencies": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + } + }, + "node_modules/reduce-css-calc/node_modules/balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" + }, + "node_modules/reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "dev": true, @@ -27251,7 +27513,9 @@ "version": "3.0.1" }, "@types/d3-geo": { - "version": "3.0.3", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "requires": { "@types/geojson": "*" } @@ -28047,6 +28311,218 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@visx/axis": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.10.1.tgz", + "integrity": "sha512-HBEDLcpZoJ16hFbkYu3S6mN5mbwlFmUWY5yN967X06RdIL4LmAG3gnZ7u4F9buA3LQo+trJXW78moN005odD4Q==", + "requires": { + "@types/react": "*", + "@visx/group": "3.3.0", + "@visx/point": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "@visx/text": "3.3.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.0" + } + }, + "@visx/curve": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.3.0.tgz", + "integrity": "sha512-G1l1rzGWwIs8ka3mBhO/gj8uYK6XdU/3bwRSoiZ+MockMahQFPog0bUkuVgPwwzPSJfsA/E5u53Y/DNesnHQxg==", + "requires": { + "@types/d3-shape": "^1.3.1", + "d3-shape": "^1.0.6" + }, + "dependencies": { + "@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "requires": { + "@types/d3-path": "^1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + } + } + }, + "@visx/grid": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-3.5.0.tgz", + "integrity": "sha512-i1pdobTE223ItMiER3q4ojIaZWja3vg46TkS6FotnBZ4c0VRDHSrALQPdi0na+YEgppASWCQ2WrI/vD6mIkhSg==", + "requires": { + "@types/react": "*", + "@visx/curve": "3.3.0", + "@visx/group": "3.3.0", + "@visx/point": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + } + }, + "@visx/group": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.3.0.tgz", + "integrity": "sha512-yKepDKwJqlzvnvPS0yDuW13XNrYJE4xzT6xM7J++441nu6IybWWwextyap8ey+kU651cYDb+q1Oi6aHvQwyEyw==", + "requires": { + "@types/react": "*", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + } + }, + "@visx/point": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.3.0.tgz", + "integrity": "sha512-03eBBIJarkmX79WbeEGTUZwmS5/MUuabbiM9KfkGS9pETBTWkp1DZtEHZdp5z34x5TDQVLSi0rk1Plg3/8RtDg==" + }, + "@visx/scale": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.5.0.tgz", + "integrity": "sha512-xo3zrXV2IZxrMq9Y9RUVJUpd93h3NO/r/y3GVi5F9AsbOzOhsLIbsPkunhO9mpUSR8LZ9TiumLEBrY+3frRBSg==", + "requires": { + "@visx/vendor": "3.5.0" + } + }, + "@visx/shape": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.5.0.tgz", + "integrity": "sha512-DP3t9jBQ7dSE3e6ptA1xO4QAIGxO55GrY/6P+S6YREuQGjZgq20TLYLAsiaoPEzFSS4tp0m12ZTPivWhU2VBTw==", + "requires": { + "@types/d3-path": "^1.0.8", + "@types/d3-shape": "^1.3.1", + "@types/lodash": "^4.14.172", + "@types/react": "*", + "@visx/curve": "3.3.0", + "@visx/group": "3.3.0", + "@visx/scale": "3.5.0", + "classnames": "^2.3.1", + "d3-path": "^1.0.5", + "d3-shape": "^1.2.0", + "lodash": "^4.17.21", + "prop-types": "^15.5.10" + }, + "dependencies": { + "@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "requires": { + "@types/d3-path": "^1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + } + } + }, + "@visx/text": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.3.0.tgz", + "integrity": "sha512-fOimcsf0GtQE9whM5MdA/xIkHMaV29z7qNqNXysUDE8znSMKsN+ott7kSg2ljAEE89CQo3WKHkPNettoVsa84w==", + "requires": { + "@types/lodash": "^4.14.172", + "@types/react": "*", + "classnames": "^2.3.1", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "reduce-css-calc": "^1.3.0" + } + }, + "@visx/vendor": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.5.0.tgz", + "integrity": "sha512-yt3SEZRVmt36+APsCISSO9eSOtzQkBjt+QRxNRzcTWuzwMAaF3PHCCSe31++kkpgY9yFoF+Gfes1TBe5NlETiQ==", + "requires": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-scale": "4.0.2", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-scale": "4.0.2", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + }, + "dependencies": { + "@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==" + }, + "@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" + }, + "d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "requires": { + "delaunator": "5" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "dev": true, @@ -34541,6 +35017,11 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, + "math-expression-evaluator": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", + "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" + }, "mathjs": { "version": "10.6.4", "requires": { @@ -35997,6 +36478,31 @@ "strip-indent": "^3.0.0" } }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" + } + } + }, + "reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "regenerate": { "version": "1.4.2", "dev": true diff --git a/package.json b/package.json index 71ff14d4e1..72a0dd5a2b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "/node_modules/", "/cypress/", "/firebase-test/", - "/functions/test/(?!(shared\\.test\\.ts)$).*$" + "/functions-v1/", + "/functions-v2/" ], "transform": { "^.+\\.tsx?$": [ @@ -227,6 +228,12 @@ "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", "@floating-ui/react": "^0.25.4", + "@visx/axis": "3.10.1", + "@visx/grid": "3.5.0", + "@visx/group": "3.3.0", + "@visx/scale": "3.5.0", + "@visx/shape": "3.5.0", + "@visx/text": "3.3.0", "chart.js": "^2.9.4", "classnames": "^2.3.1", "client-oauth2": "^4.3.3", diff --git a/scripts/ai/clean-docs-without-metadata.ts b/scripts/ai/clean-docs-without-metadata.ts new file mode 100644 index 0000000000..76d9e40db1 --- /dev/null +++ b/scripts/ai/clean-docs-without-metadata.ts @@ -0,0 +1,258 @@ +#!/usr/bin/node + +// This script finds documents without metadata in the realtime database. +// If the deleteTypes array is uncommented, it will delete these documents. + +// to run this script type the following in the terminal +// cf. https://stackoverflow.com/a/66626333/16328462 +// $ cd scripts/ai +// $ npx tsx clean-docs-without-metadata.ts + +import admin from "firebase-admin"; + +import { getFirebaseBasePath, getScriptRootFilePath, prettyDuration, + remapFirebaseClassPublications, remapFirebaseProblemDocPublications } from "../lib/script-utils.js"; + +// Load the service account key JSON file. +import { getClassKeys } from "../lib/firebase-classes.js"; + +// The portal to get documents from. For example, "learn.concord.org". +const portal = "learn.concord.org"; +// The demo name to use. Make falsy to not use a demo. +// const demo = "TAGCLUE"; +const demo = false; + +// Make falsy to include all documents +const documentLimit = false; +// const documentLimit = 10000; + +// List of types to delete if they appear safe to do so. +const deleteTypes = [ + // "problem", + // "planning", + // "learningLog", + // "publication" +]; + +// If a problem or planning doc doesn't have metadata and there is an offering for +// the user that also doesn't have type specific metadata for any problem or planning +// docs, perhaps the doc is supposed to go in this "empty slot". +// Or if there is a learningLog that doesn't have metadata and there is no learningLog +// metadata for this user. +// For the problem and planning docs this hasn't been practical to figure out. +// There are usually multiple empty slots. +// For learningLogs there haven't been any cases like this. +// Additionally these docs would not be accessible to the user +// after they were created. So there isn't much point in keeping them around. +const deleteDefaultDocsEvenIfThereIsEmptySlot = true; +// const deleteDefaultDocsEvenIfThereIsEmptySlot = false; + +console.log(`*** Starting to Download Documents ***`); + +const startTime = Date.now(); +let documentsProcessed = 0; +let undefinedDocuments = 0; +let failedDocuments = 0; +let emptyDocuments = 0; + +const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; + +const firebaseBasePath = getFirebaseBasePath(portal, demo); + +const {classKeys, accessTime, fetchTime} = await getClassKeys(firebaseBasePath); + +// Fetch the service account key JSON file contents; must be in same folder as script +const serviceAccountFile = getScriptRootFilePath("serviceAccountKey.json"); +const credential = admin.credential.cert(serviceAccountFile); +// Initialize the app with a service account, granting admin privileges +admin.initializeApp({ + credential, + databaseURL +}); + +const credentialTime = Date.now(); + +// CHECKME: what about cross class supports? +// They might be saved as a supportPublication with an offering but then there +// is a metadata file that has extra information about it. + +for (const key of Object.keys(classKeys)) { + const getClassValue = async (prop: string) => { + const snapshot = await admin.database().ref(`${firebaseBasePath}/${key}/${prop}`).once("value"); + return snapshot.val(); + }; + + if (documentLimit && documentsProcessed >= documentLimit) break; + const users = await getClassValue("users"); + const offerings = await getClassValue("offerings"); + const fbPersonalPublications = await getClassValue("personalPublications"); + const personalPublications = remapFirebaseClassPublications(fbPersonalPublications); + const fbLearningLogPublications = await getClassValue("publications"); + const learningLogPublications = remapFirebaseClassPublications(fbLearningLogPublications); + + const problemDocPublications = {}; + for (const [offeringId, offering] of Object.entries(offerings)) { + const fbProblemDocPublications = (offering as any).publications; + if (!fbProblemDocPublications) continue; + problemDocPublications[offeringId] = remapFirebaseProblemDocPublications(fbProblemDocPublications); + } + + for (const [userId, user] of Object.entries(users)) { + if (documentLimit && documentsProcessed >= documentLimit) break; + if (!user.documents) continue; + for (const [docId, doc] of Object.entries(user.documents)) { + if (documentLimit && documentsProcessed >= documentLimit) break; + documentsProcessed++; + + const docPath = `${firebaseBasePath}/${key}/users/${userId}/documents/${docId}`; + const content = doc.content as string | undefined; + let parsedContent; + let tiles; + if (!content) { + undefinedDocuments++; + } else { + try { + parsedContent = JSON.parse(content); + tiles = Object.values(parsedContent.tileMap); + if (tiles.length === 0) { + emptyDocuments++; + } + } catch (e) { + failedDocuments++; + } + } + + const documentMetadata = user.documentMetadata[docId]; + + // We only care about docs without metadata + if (documentMetadata) continue; + + const deleteDoc = async () => { + if (deleteTypes.includes(doc.type)) { + try { + await admin.database().ref(docPath).remove(); + console.log("deleted", docPath); + } catch (e) { + console.log("failed to delete", docPath, e); + } + } else { + console.log("would delete", docPath); + } + }; + + const personalDocMetadata = user.personalDocs?.[docId]; + const learningLogMetadata = user.learningLogs?.[docId]; + + const hasContent = !!tiles && (tiles.length > 0); + const tools = new Set(); + if (hasContent) { + for (const tile of tiles) { + const { type } = tile.content; + if (type === "Placeholder") continue; + tools.add(type); + } + } + + console.log(documentsProcessed, "No metadata", `${key}/users/${userId}/documentMetadata/${docId}`, + {type: doc.type, hasContent }); + + if (tools.size) { + console.log("tools", [...tools]); + } + + const typeSpecificMetadata = { offerings: {}} as any; + if (learningLogMetadata) typeSpecificMetadata.learningLogMetadata = learningLogMetadata; + if (personalDocMetadata) typeSpecificMetadata.personalDocMetadata = personalDocMetadata; + + // Look for type specific metadata + // We have to search through several places since we don't have an offering id + for (const [offeringId, offering] of Object.entries(offerings)) { + const offeringUser = (offering as any).users?.[userId]; + const problemMetadata = offeringUser?.documents?.[docId]; + const planningMetadata = offeringUser?.planning?.[docId]; + if (problemMetadata || planningMetadata) { + typeSpecificMetadata.offerings[offeringId] = {}; + } + if (problemMetadata) { + typeSpecificMetadata.offerings[offeringId].problemMetadata = problemMetadata; + } + if (planningMetadata) { + typeSpecificMetadata.offerings[offeringId].planningMetadata = planningMetadata; + } + } + + for (const [offeringId, problemDocPublicationsOffering] of Object.entries(problemDocPublications)) { + const problemDocPublication = problemDocPublicationsOffering?.[docId]; + if (problemDocPublication) { + if (typeSpecificMetadata.offerings[offeringId]) { + typeSpecificMetadata.offerings[offeringId] = {}; + } + typeSpecificMetadata.offerings[offeringId].problemDocPublication = problemDocPublication; + } + } + + const personalPublication = personalPublications?.[docId]; + if (personalPublication) { + typeSpecificMetadata.personalPublication = personalPublication; + } + const learningLogPublication = learningLogPublications?.[docId]; + if (learningLogPublication) { + typeSpecificMetadata.learningLogPublication = learningLogPublication; + } + + if (Object.keys(typeSpecificMetadata).length > 1 || Object.keys(typeSpecificMetadata.offerings).length > 0) { + // So far none of the docs without generic metadata have any typeSpecific Metadata + console.log("typeSpecific", typeSpecificMetadata); + + // If there is type specific metadata don't try to delete this document + continue; + } + + // If the doc type is a problem or planning check to see if there is one configured for all + // of the offerings of this user. If there isn't one, perhaps this doc should be saved. + // + if (["problem", "planning"].includes(doc.type)) { + const offeringStats = {}; + for (const [offeringId, offering] of Object.entries(offerings)) { + const offeringUser = (offering as any).users?.[userId]; + if (doc.type === "problem") { + offeringStats[offeringId] = Object.keys(offeringUser?.documents || {}).length; + } + if (doc.type === "planning") { + offeringStats[offeringId] = Object.keys(offeringUser?.planning || {}).length; + } + } + console.log("offeringStats", offeringStats); + if (deleteDefaultDocsEvenIfThereIsEmptySlot || !Object.values(offeringStats).includes(0)) { + await deleteDoc(); + } + + } + if (doc.type === "learningLog") { + // undefined means there isn't even a learningLogs map in the database + const numLearningLogs = user.learningLogs && Object.keys(user.learningLogs || {}).length; + console.log("num learningLogs", numLearningLogs); + if (deleteDefaultDocsEvenIfThereIsEmptySlot || numLearningLogs) { + await deleteDoc(); + } + } + if (doc.type === "publication") { + await deleteDoc(); + } + } + + } +} + +const endTime = Date.now(); +console.log(`***** End script *****`); +console.log(`- Time to access token: ${prettyDuration(accessTime - startTime)}`); +console.log(`- Time to fetch documents: ${prettyDuration(fetchTime - startTime)}`); +console.log(`- Time to get credential: ${prettyDuration(credentialTime - startTime)}`); +console.log(`- Total Time: ${prettyDuration(endTime - startTime)}`); +console.log(`Documents downloaded: ${documentsProcessed}`); +console.log(`Undefined documents: ${undefinedDocuments}`); +console.log(`Empty documents: ${emptyDocuments}`); +console.log(`Failed to process: ${failedDocuments}`); + +process.exit(0); diff --git a/scripts/ai/download-documents-with-info.ts b/scripts/ai/download-documents-with-info.ts index 1dc496b2b7..de18463259 100644 --- a/scripts/ai/download-documents-with-info.ts +++ b/scripts/ai/download-documents-with-info.ts @@ -14,13 +14,15 @@ import admin from "firebase-admin"; import stringify from "json-stringify-pretty-compact"; import { datasetPath, networkFileName } from "./script-constants.js"; -import { getFirebaseBasePath, getScriptRootFilePath, prettyDuration } from "../lib/script-utils.js"; +import { getFirebaseBasePath, getScriptRootFilePath, prettyDuration, + remapFirebaseClassPublications, remapFirebaseProblemDocPublications } from "../lib/script-utils.js"; // Load the service account key JSON file. import { getClassKeys } from "../lib/firebase-classes.js"; // The portal to get documents from. For example, "learn.concord.org". const portal = "learn.concord.org"; +// const portal = "learn.portal.staging.concord.org"; // The demo name to use. Make falsy to not use a demo. // const demo = "TAGCLUE"; const demo = false; @@ -52,45 +54,6 @@ admin.initializeApp({ databaseURL }); -/** - * Firebase publications are stored with different keys than their document - * id for some reason. In some cases the real document id is in self.documentKey - * so we make a map with that documentKey as the key of the map. - * - * @param fbPublications - */ -function remapFirebaseClassPublications(fbPublications: Record) { - if (!fbPublications) return undefined; - const publications = {}; - for (const [fbId, publication] of Object.entries(fbPublications)) { - if (!publication?.self?.documentKey) { - console.log("Invalid publication found: ", fbId); - continue; - } - publications[publication.self.documentKey] = publication; - } - return publications; -} - -/** - * Firebase publications are stored with different keys than their document - * id for some reason. In some cases the real document id is in documentKey - * so we make a map with that documentKey as the key of the map. - * @param fbPublications - */ -function remapFirebaseProblemDocPublications(fbPublications: Record) { - if (!fbPublications) return undefined; - const publications = {}; - for (const [fbId, publication] of Object.entries(fbPublications)) { - if (!publication?.documentKey) { - console.log("Invalid publication found: ", fbId); - continue; - } - publications[publication.documentKey] = publication; - } - return publications; -} - const credentialTime = Date.now(); // CHECKME: what about cross class supports? @@ -135,6 +98,7 @@ for (const key of Object.keys(classKeys)) { for (const [userId, user] of Object.entries(users)) { if (documentLimit && documentsProcessed >= documentLimit) break; // console.log(` ${userId}`); + if (!user.documents) continue; for (const [docId, doc] of Object.entries(user.documents)) { if (documentLimit && documentsProcessed >= documentLimit) break; diff --git a/scripts/ai/get-offering-info.ts b/scripts/ai/get-offering-info.ts index 8c03b208cc..3a327123d7 100644 --- a/scripts/ai/get-offering-info.ts +++ b/scripts/ai/get-offering-info.ts @@ -1,28 +1,35 @@ #!/usr/bin/node -// This script counts documents downloaded with download-documents-with-info.ts, -// then prints the counts to the terminal. -// It is currently set up to count the types of documents in the document collection, as well as how many -// documents have titles. +// This script parses documents downloaded with download-documents-with-info.ts, uses the information in them +// to fetch related offering and class data from a portal, and writes that data to local files that can be used by +// other scripts to update Firestore metadata documents. // to run this script type the following in the terminal // cf. https://stackoverflow.com/a/66626333/16328462 // Change sourceDirectory to be the name of the directory containing your documents -// Change targetTileTypes to be a list of the tile types you want to count (like ["Geometry", "Text", "Table"]) -// Set aiService to be whichever service you're interested in. This will determine the format of the output file. // $ cd scripts/ai // $ npx tsx get-offering-info.ts import fs from "fs"; +import admin from "firebase-admin"; import path from "path"; import stringify from "json-stringify-pretty-compact"; -import { fetchOffering } from "../lib/fetch-offering.js"; -import { prettyDuration } from "../lib/script-utils.js"; +import { fetchPortalClass, fetchPortalOffering } from "../lib/fetch-portal-entity.js"; +import { getFirestoreUsersPath, getScriptRootFilePath, prettyDuration } from "../lib/script-utils.js"; + +const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; +// Fetch the service account key JSON file contents +const serviceAccountFile = getScriptRootFilePath("serviceAccountKey.json"); +const credential = admin.credential.cert(serviceAccountFile); +// Initialize the app with a service account, granting admin privileges +admin.initializeApp({ + credential, + databaseURL +}); import { datasetPath } from "./script-constants.js"; -// const sourceDirectory = "dataset1720814823478"; -const sourceDirectory = "dataset1721059336040"; +const sourceDirectory = "dataset1724085367882"; // src/public/ai/dataset1720819925834 // The number of files to process in parallel const fileBatchSize = 8; @@ -96,30 +103,60 @@ console.log(`*** Found ${offeringIds.size} unique offerings ***`); // update this list in the Firestore /classes/[class doc], and remove the // teacher list from the metadata documents. interface OfferingInfo { - activity_url, - clazz_id, - clazz_hash + activity_url: string, + clazz_id: string, + clazz_hash: string +} + +interface IClassTeacher { + user_id: string +} + +interface IPortalClassData { + class_hash: string, + name: string, + teachers: IClassTeacher[], + uri: string +} + +interface ClassInfo { + context_id: string, + id: string, + name: string, + networks: string[], + teachers: string[], + uri: string } + const offeringInfo: Record = {}; +const classInfo: Record = {}; const networkInfoContent = fs.readFileSync(path.resolve(sourcePath, "network.json"), "utf8"); const networkInfo = JSON.parse(networkInfoContent); -const { demo } = networkInfo; +const { demo, portal } = networkInfo; if (demo) { for (const offeringId of offeringIds) { - let [full, unitCode, investigation, problem] = offeringId.match(/(.*)(\d)(\d\d)/); - if (!unitCode) unitCode = "sas"; - problem = stripLeadingZero(problem); - console.log({unitCode, investigation, problem}); + const match = offeringId.match(/(.*)(\d)(\d\d)/); + if (match) { + let [full, unitCode, investigation, problem] = match; + if (!unitCode) unitCode = "sas"; + problem = stripLeadingZero(problem); + console.log({unitCode, investigation, problem}); + } } } else { let numFetchedOfferings = 0; + const clazzes = new Set(); for (const offeringId of offeringIds) { - const offering = await fetchOffering("https://learn.concord.org", offeringId); + if (!offeringId) continue; + const offering = await fetchPortalOffering(`https://${portal}`, offeringId); if (!offering) continue; const {activity_url, clazz_id, clazz_hash} = offering; + if (!clazzes.has(clazz_id)) { + clazzes.add(clazz_id); + } offeringInfo[offeringId] = { activity_url, clazz_id, clazz_hash }; @@ -130,9 +167,69 @@ if (demo) { } // Write offering info as a JSON file for use by later scripts + console.log("Preparing to write offering file."); const offeringInfoFile = `${sourcePath}/offering-info.json`; fs.writeFileSync(offeringInfoFile, stringify(offeringInfo)); + const processedTeachers = new Map(); + + const collectionUrl = getFirestoreUsersPath(portal, demo); + const documentCollection = admin.firestore().collection(collectionUrl); + + for (const clazz_id of clazzes) { + if (!clazz_id) continue; + + const clazzData = await fetchPortalClass(`https://${portal}`, clazz_id); + if (Object.keys(clazzData).length === 0) continue; + + const { class_hash, name, teachers, uri } = clazzData as IPortalClassData; + const teacherNetworks = new Set(); + const teacherIds: string[] = []; + + // Prepare an array of teacher user IDs that need to be fetched + const teacherFetchPromises = teachers.map(async (classTeacher: IClassTeacher) => { + const { user_id } = classTeacher; + teacherIds.push(user_id); + + if (!processedTeachers.has(user_id)) { + const userQuery = await documentCollection.where("uid", "==", String(user_id)).get(); + if (userQuery.empty) { + console.log(`No user found with uid ${user_id}`); + return; + } + + const userDoc = userQuery.docs[0].data(); + const { networks } = userDoc; + + for (const network of networks) { + teacherNetworks.add(network); + } + + processedTeachers.set(user_id, networks); + } else { + const networks = processedTeachers.get(user_id); + for (const network of networks) { + teacherNetworks.add(network); + } + } + }); + + await Promise.all(teacherFetchPromises); + + classInfo[clazz_id] = { + context_id: class_hash, + id: clazz_id, + name, + networks: Array.from(teacherNetworks), + teachers: teacherIds, + uri + }; + } + + // For each classInfo write class info as a JSON file for use by later scripts + const classInfoFile = `${sourcePath}/class-info.json`; + fs.writeFileSync(classInfoFile, stringify(classInfo)); + const finishedFetchingOfferings = Date.now(); const fetchingDuration = finishedFetchingOfferings - finishedLoading; console.log(`*** Fetched ${numFetchedOfferings} offerings in ${prettyDuration(fetchingDuration)}s ***`); diff --git a/scripts/ai/offering-json-to-csv.ts b/scripts/ai/offering-json-to-csv.ts new file mode 100644 index 0000000000..0e07f4e1e7 --- /dev/null +++ b/scripts/ai/offering-json-to-csv.ts @@ -0,0 +1,20 @@ +import fs from "fs"; +import { getClueBranch, getUnitParam, getUnitBranch, getUnitCode } from "../lib/script-utils.js"; + +import { datasetPath } from "./script-constants.js"; +const sourceDirectory = "dataset1724085367882"; +const sourcePath = `${datasetPath}${sourceDirectory}`; +const offeringInfoFile = `${sourcePath}/offering-info.json`; +const offeringInfo = JSON.parse(fs.readFileSync(offeringInfoFile, "utf8")); + +console.log("offering_id, activity_url, class_id, clazz_hash, clue_branch, unit_param, unit_branch, unit_code"); +Object.entries(offeringInfo).forEach(([offering_id, offering]) => { + const {activity_url, clazz_id, clazz_hash} = offering as any; + const clueBranch = getClueBranch(activity_url); + const unitParam = getUnitParam(activity_url); + const unitBranch = getUnitBranch(unitParam); + const unitCode = getUnitCode(unitParam); + console.log( + `${offering_id}, ${activity_url}, ${clazz_id}, ${clazz_hash}, ` + + `${clueBranch}, ${unitParam}, ${unitBranch}, ${unitCode}`); +}); diff --git a/scripts/ai/update-class-metadata.ts b/scripts/ai/update-class-metadata.ts new file mode 100644 index 0000000000..6d0e989e4a --- /dev/null +++ b/scripts/ai/update-class-metadata.ts @@ -0,0 +1,136 @@ +#!/usr/bin/node + +// This script uses the downloaded documents to get class and network info +// then creates or updates Firestore class metadata documents + +// to run this script type the following in the terminal +// cf. https://stackoverflow.com/a/66626333/16328462 +// $ npx tsx ai/update-class-metadata.ts + +import fs from "fs"; +import admin from "firebase-admin"; + +import { datasetPath, networkFileName } from "./script-constants.js"; +import { getFirestoreClassesPath, getScriptRootFilePath } from "../lib/script-utils.js"; + +// The directory containing the documents you're interested in. +// This should be the output of download-documents-with-info.ts. +const sourceDirectory = "dataset1724113771908"; +const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; + +// Fetch the service account key JSON file contents +const credential = admin.credential.cert(getScriptRootFilePath("serviceAccountKey.json")); +// Initialize the app with a service account, granting admin privileges +admin.initializeApp({ + credential, + databaseURL +}); + +const sourcePath = `${datasetPath}${sourceDirectory}`; + +// Get network info from portal file. This should have been created by download-documents-with-info.ts. +function getNetworkInfo() { + const networkFile = `${sourcePath}/${networkFileName}`; + if (fs.existsSync(networkFile)) { + return JSON.parse(fs.readFileSync(networkFile, "utf8")); + } +} +const { portal, demo } = getNetworkInfo(); + +const collectionUrl = getFirestoreClassesPath(portal, demo); +const documentCollection = admin.firestore().collection(collectionUrl); + +let processedClasses = 0; +let metadataUpdated = 0; +let metadataCreated = 0; + +async function saveCurrentMetadata() { + const timestamp = new Date().toISOString().replace(/:|T/g, "-").replace(/\.\d{3}Z$/, ""); + const documentSnapshots = await documentCollection.get(); + const metadata = {}; + documentSnapshots.forEach(doc => { + metadata[doc.id] = doc.data(); + }); + const metadataFilePath = `${sourcePath}/class-metadata-backup-${timestamp}.json`; + fs.writeFileSync(metadataFilePath, JSON.stringify(metadata, null, 2)); +} + +async function processFile() { + const filePath = `${sourcePath}/class-info.json`; + const content = fs.readFileSync(filePath, "utf8"); + const parsedContent = JSON.parse(content); + + for (const classId in parsedContent) { + const { + context_id, + name, + networks, + uri + } = parsedContent[classId]; + const id = String(parsedContent[classId].id); + const teachers = parsedContent[classId].teachers.map(teacher => String(teacher)); + + processedClasses++; + + const documentSnapshots = await documentCollection.where("id", "==", id).get(); + + const createClassDoc = async () => { + const metaData = { + context_id, + id, + teachers, + name, + networks, + uri + }; + const metaDataDocId = context_id; + const newMetaDataDoc = documentCollection.doc(metaDataDocId); + await newMetaDataDoc.create(metaData); + console.log("Created new class metadata", metaDataDocId); + metadataCreated++; + }; + + // There can be multiple class metadata documents for each actual class. Note that the name/path for these + // Firestore documents may be "[network name]_[class hash]" and/or simply "[class hash]". + // For now we just update all of these documents. + + let hasClassDocWithSimpleId = false; + for (const doc of documentSnapshots.docs) { + if (doc.id === context_id) hasClassDocWithSimpleId = true; + + const requiredMatches = [ + { field: "context_id", expected: context_id, actual: doc.data().context_id }, + { field: "id", expected: id, actual: doc.data().id }, + { field: "uri", expected: uri, actual: doc.data().uri } + ]; + + let hasMismatch = false; + for (const { field, expected, actual } of requiredMatches) { + if (expected !== actual) { + console.error(`Skipping update of ${doc.id} due to ${field} mismatch. Expected ${expected}, got ${actual}.`); + hasMismatch = true; + } + } + if (hasMismatch) continue; + + await doc.ref.update({ name, networks, teachers } as any); + console.log(context_id, doc.id, "Updated existing class metadata with", { name, networks, teachers }); + metadataUpdated++; + } + + if (!hasClassDocWithSimpleId) { + await createClassDoc(); + } + } +} + +console.log("*** Recording current Firestore class metadata to local file ***"); +await saveCurrentMetadata(); +console.log("*** Finished recording current Firestore class metadata to local file ***"); + +console.log(`*** Loading downloaded CLUE class info ***`); +await processFile(); + +console.log(`*** Processed ${processedClasses} classes ***`); +console.log(`*** Created ${metadataCreated} metadata docs ***`); +console.log(`*** Updated ${metadataUpdated} metadata docs ***`); diff --git a/scripts/ai/update-metadata.ts b/scripts/ai/update-metadata.ts index b57832fcd9..0baa6593bd 100644 --- a/scripts/ai/update-metadata.ts +++ b/scripts/ai/update-metadata.ts @@ -11,12 +11,12 @@ import fs from "fs"; import admin from "firebase-admin"; import { datasetPath, networkFileName } from "./script-constants.js"; -import { getFirestoreBasePath, getScriptRootFilePath } from "../lib/script-utils.js"; +import { getFirestoreBasePath, getProblemDetails, getScriptRootFilePath } from "../lib/script-utils.js"; // The directory containing the documents you're interested in. // This should be the output of download-documents.ts. // Each document should be named like documentID.txt, where ID is the document's id in the database. -const sourceDirectory = "dataset1721156514478"; +const sourceDirectory = "dataset1724185627549"; console.log(`*** Starting to Update Metadata ***`); @@ -55,17 +55,17 @@ function getNetworkInfo() { } const { portal, demo } = getNetworkInfo(); -// For now, only run for demo spaces -if (!demo) { - console.error("demo not defined, exiting"); - process.exit(1); -} - console.log(`***** Reading doc and updating metadata *****`); const collectionUrl = getFirestoreBasePath(portal, demo); console.log(`*** Updating docs in ${collectionUrl} ***`); const documentCollection = admin.firestore().collection(collectionUrl); +const offeringInfoFile = `${sourcePath}/offering-info.json`; +let offeringInfo; +if (!demo) { + offeringInfo = JSON.parse(fs.readFileSync(offeringInfoFile, "utf8")); +} + let processedFiles = 0; let metadataUpdated = 0; let metadataCreated = 0; @@ -98,10 +98,19 @@ async function processFile(file: string) { processedFiles++; const tiles = documentContent?.tileMap ? Object.values(documentContent.tileMap) : []; - const tileTypes = []; + const tools = []; for (const tile of tiles) { - if (!tileTypes.includes(tile.content.type)) { - tileTypes.push(tile.content.type); + if (!tools.includes(tile.content.type)) { + tools.push(tile.content.type); + } + } + + const annotations = documentContent?.annotations ? Object.values(documentContent.annotations) : []; + for (const annotation of annotations) { + // for now we only want Sparrow annotations + // we might want to change this if we want to count other types in the future + if (annotation.type === "arrowAnnotation" && !tools.includes("Sparrow")) { + tools.push("Sparrow"); } } @@ -112,35 +121,59 @@ async function processFile(file: string) { unit: null }; - if (offeringId) { - // Extract the unit, investigation, and problem from the offeringId. - // The `offeringId` structure can vary. In some cases, there is no unit code. There are also cases where - // there is no investigation number. For example, in demo mode if the unit is not specified, there will - // be no unit value. And in the case where the investigation is 0 (like with the Intro to CLUE - // investigation in the Introduction to CLUE unit) the investigation will be undefined. In those cases, - // we default to "sas" for the unit and "0" for the investigation. - let unitCode = ""; - let investigation = ""; - let problem = ""; - const match = offeringId.match(/(.*?)(\d)(\d\d)$/); - - if (match) { - [, unitCode, investigation, problem] = match; - } else { - investigation = "0"; - problem = offeringId.match(/\d+/)?.[0] || ""; + if (!demo) { + const offering = offeringInfo[offeringId]; + if (offering) { + const { activity_url } = offering; + try { + const { investigation, problem, unit } = getProblemDetails(activity_url); + + if (unit && !problem) { + console.log("Found unit but not problem in activity_url", activity_url); + } + + unitFields = { + problem, + investigation, + unit + }; + } catch (e) { + console.error(e, {offeringId, offering}); + console.log("Skipping document because it has an invalid offering", {offeringId, offering}); + return; + } } + } else { + if (offeringId) { + // Extract the unit, investigation, and problem from the offeringId. + // The `offeringId` structure can vary. In some cases, there is no unit code. There are also cases where + // there is no investigation number. For example, in demo mode if the unit is not specified, there will + // be no unit value. And in the case where the investigation is 0 (like with the Intro to CLUE + // investigation in the Introduction to CLUE unit) the investigation will be undefined. In those cases, + // we default to "sas" for the unit and "0" for the investigation. + let unitCode = ""; + let investigation = ""; + let problem = ""; + const match = offeringId.match(/(.*?)(\d)(\d\d)$/); + + if (match) { + [, unitCode, investigation, problem] = match; + } else { + investigation = "0"; + problem = offeringId.match(/\d+/)?.[0] || ""; + } - if (!unitCode) unitCode = "sas"; - problem = stripLeadingZero(problem); + if (!unitCode) unitCode = "sas"; + problem = stripLeadingZero(problem); - console.log({ unitCode, investigation, problem }); + console.log({ unitCode, investigation, problem }); - unitFields = { - problem, - investigation, - unit: unitCode - }; + unitFields = { + problem, + investigation, + unit: unitCode + }; + } } // TODO: download docs in batches instead of one at a time @@ -193,27 +226,26 @@ async function processFile(file: string) { properties: {}, strategies, - // For now we just handle demo documents where the teachers are hardcoded. - // To support Portal launches we'll either have to get the list of teachers from the offering - // info, or refactor the code so this teacher list isn't needed here. See: - // https://docs.google.com/document/d/1VDr-nkthu333eVD0BQXPYPVD8kt60qkMYq2jRkXza9c/edit#heading=h.pw87siu4ztwo - teachers: ["1001", "1002", "1003"], - tileTypes, + tools, title: documentTitle || null, type: documentType, uid: userId, visibility }; + if (!documentType) { + console.log("Skipping document because it has no documentType", documentId); + return; + } + // Use a prefix of `uid:[owner_uid]` for metadata documents that we create for more // info see: // https://docs.google.com/document/d/1VDr-nkthu333eVD0BQXPYPVD8kt60qkMYq2jRkXza9c/edit#heading=h.5t2tt6igiiou const metaDataDocId = `uid:${userId}_${documentId}`; - console.log(documentId, "Created new metadata", metaDataDocId); const newMetaDataDoc = documentCollection.doc(metaDataDocId); await newMetaDataDoc.create(metaData); - console.log(documentId, "Created new metadata", metaDataDocId); + console.log(processedFiles, documentId, "Created new metadata", metaDataDocId); metadataCreated++; } else { // There can be multiple metadata documents for each actual document. @@ -221,12 +253,14 @@ async function processFile(file: string) { // stops creating multiple copies. See: // https://docs.google.com/document/d/1VDr-nkthu333eVD0BQXPYPVD8kt60qkMYq2jRkXza9c/edit#heading=h.5t2tt6igiiou documentSnapshots.forEach(doc => { - doc.ref.update(unitFields as any); - console.log(documentId, doc.id, "Updated metadata with", unitFields); - doc.ref.update({ strategies, tileTypes } as any); - console.log(documentId, doc.id, "Updated metadata with", { strategies, tileTypes }); - doc.ref.update({ visibility } as any); - console.log(documentId, doc.id, "Updated metadata with", { visibility }); + const newMetadata = { + ...unitFields, + strategies, + tools, + visibility + }; + doc.ref.update(newMetadata as any); + console.log(processedFiles, documentId, doc.id, "Updated metadata with", newMetadata); metadataUpdated++; }); } @@ -249,6 +283,7 @@ const fileBatchSize = 8; await new Promise((resolve) => { // Process every file in the source directory fs.readdir(sourcePath, async (_error, files) => { + console.log(`*** Processing ${files.length} documents ***`); for (const file of files) { checkedFiles++; fileBatch.push(file); @@ -273,5 +308,3 @@ function stripLeadingZero(input: string) { console.log(`*** Processed ${processedFiles} downloaded CLUE docs ***`); console.log(`*** Created ${metadataCreated} metadata docs ***`); console.log(`*** Updated ${metadataUpdated} metadata docs ***`); - -process.exit(0); diff --git a/scripts/clean-firebase-roots.ts b/scripts/clean-firebase-roots.ts new file mode 100644 index 0000000000..bd39912791 --- /dev/null +++ b/scripts/clean-firebase-roots.ts @@ -0,0 +1,33 @@ +#!/usr/bin/node + +// This script cleans the roots out of the QA or Dev sections of +// the Firebase Realtime database and Firestore. + +// to run this script type the following in the terminal +// cf. https://stackoverflow.com/a/66626333/16328462 +// $ cd scripts +// $ npx tsx clean-firebase-roots.ts + +import admin from "firebase-admin"; +import { deleteApp } from "firebase-admin/app"; +import {cleanFirebaseRoots} from "../shared/clean-firebase-roots.js"; +import { getScriptRootFilePath } from "./lib/script-utils.js"; + +const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; + +const serviceAccountFile = getScriptRootFilePath("serviceAccountKey.json"); +const credential = admin.credential.cert(serviceAccountFile); +// Initialize the app with a service account, granting admin privileges +const fbApp = admin.initializeApp({ + credential, + databaseURL +}); + +await cleanFirebaseRoots({ + appMode: "qa", + hoursAgo: 90.7, + logger: console, + dryRun: true +}); + +await deleteApp(fbApp); diff --git a/scripts/lib/fetch-portal-entity.ts b/scripts/lib/fetch-portal-entity.ts new file mode 100644 index 0000000000..382210bbc4 --- /dev/null +++ b/scripts/lib/fetch-portal-entity.ts @@ -0,0 +1,27 @@ +import "./dot-env.js"; + +async function fetchPortalEntity(portal: string, entityType: string, resourceId: string) { + const accessToken = process.env.PORTAL_ACCESS_TOKEN; + const fetchURL = `${portal}/api/v1/${entityType}/${resourceId}`; + console.log("Fetching", fetchURL); + const response = await fetch(fetchURL, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + const json = await response.json(); + if ("success" in json && !json.success) { + throw new Error(`Failed to fetch ${entityType}`, {cause: json}); + } + return json; +} + +export async function fetchPortalOffering(portal: string, offeringId: string) { + return fetchPortalEntity(portal, "offerings", offeringId); +} + +export async function fetchPortalClass(portal: string, classId: string) { + return fetchPortalEntity(portal, "classes", classId); +} diff --git a/scripts/lib/script-utils.ts b/scripts/lib/script-utils.ts index 9b3b77e7b0..b155dc356c 100644 --- a/scripts/lib/script-utils.ts +++ b/scripts/lib/script-utils.ts @@ -30,6 +30,110 @@ export function getFirestoreBasePath(portal: string, demo?: string | boolean) { : `authed/${portal.replace(/\./g, "_")}/documents`; } +export function getFirestoreUsersPath(portal: string, demo?: string | boolean) { + return demo + ? `demo/${demo}/users` + : `authed/${portal.replace(/\./g, "_")}/users`; +} + +export function getFirestoreClassesPath(portal: string, demo?: string | boolean) { + return demo + ? `demo/${demo}/classes` + : `authed/${portal.replace(/\./g, "_")}/classes`; +} + export function getScriptRootFilePath(filename: string) { return path.resolve(scriptsRoot, filename); } + +// eslint-disable-next-line prefer-regex-literals +const clueBranchRegExp = new RegExp("^https://[^/]*(/[^?]*)"); +export function getClueBranch(activityUrl: string) { + return clueBranchRegExp.exec(activityUrl)?.[1]; +} + +// eslint-disable-next-line prefer-regex-literals +const unitParamRegExp = new RegExp("unit=([^&]*)"); +export function getUnitParam(activityUrl: string) { + return unitParamRegExp.exec(activityUrl)?.[1]; +} + +// eslint-disable-next-line prefer-regex-literals +const unitBranchRegExp = new RegExp("/branch/[^/]*"); +export function getUnitBranch(unitParam: string | undefined) { + if (unitParam?.startsWith("https://")) { + return unitBranchRegExp.exec(unitParam)?.[0]; + } else { + return ""; + } +} + +// eslint-disable-next-line prefer-regex-literals +const unitCodeRegExp = new RegExp("/([^/]*)/content.json"); +export function getUnitCode(unitParam: string | undefined) { + if (unitParam?.startsWith("https://")) { + const unitCode = unitCodeRegExp.exec(unitParam)?.[1]; + return unitCode ? unitCode : null; + } else { + return unitParam ? unitParam : null; + } +} + +export function getProblemDetails(url: string) { + const activityURL = new URL(url); + const urlParams = activityURL.searchParams; + const unitParam = urlParams.get("unit"); + // The unit param's value may be a unit code or a full url, so we make sure to get just the unit code + const unit = getUnitCode(unitParam); + const investigationAndProblem = urlParams.get("problem"); + const [investigation, problem] = investigationAndProblem ? investigationAndProblem.split(".") : [null, null]; + return { investigation, problem, unit }; +} + +/** + * Create a new Record based on a passed in Record. The keys in the new Record + * are computed by the passed in getNewKey function. + * If getNewKey returns a falsely value the entry is skipped and it is logged + * to the console. + * + * @param originalMap + * @param getNewKey + * @returns + */ +export function remap( + originalMap: Record, + getNewKey: (value: any) => string | undefined +) { + if (!originalMap) return undefined; + const newMap = {}; + for (const [originalKey, value] of Object.entries(originalMap)) { + const newKey = getNewKey(value); + if (!newKey) { + console.log("Invalid value found: ", originalKey, value); + continue; + } + newMap[newKey] = value; + } + return newMap; +} + +/** + * Firebase publications are stored with different keys than their document + * id for some reason. In some cases the real document id is in self.documentKey + * so we make a map with that documentKey as the key of the map. + * + * @param fbPublications + */ +export function remapFirebaseClassPublications(fbPublications: Record) { + return remap(fbPublications, (metadata) => metadata?.self?.documentKey); +} + +/** + * Firebase publications are stored with different keys than their document + * id for some reason. In some cases the real document id is in documentKey + * so we make a map with that documentKey as the key of the map. + * @param fbPublications + */ +export function remapFirebaseProblemDocPublications(fbPublications: Record) { + return remap(fbPublications, (metadata) => metadata?.documentKey); +} diff --git a/shared/clean-firebase-roots.ts b/shared/clean-firebase-roots.ts new file mode 100644 index 0000000000..7b300e1149 --- /dev/null +++ b/shared/clean-firebase-roots.ts @@ -0,0 +1,50 @@ +// This requires the modern firebase-admin, so it can't be used by functions-v1 +import {Timestamp, getFirestore} from "firebase-admin/firestore"; +import {getDatabase} from "firebase-admin/database"; + +const HOUR = 1000 * 60 * 60; + +interface Logger { + info(...args: any[]): void; +} + +interface Params { + appMode: "qa" | "dev"; + hoursAgo: number; + logger: Logger; + dryRun?: boolean; +} + +export async function cleanFirebaseRoots( + { appMode, hoursAgo, logger, dryRun }: Params +) { + + // Be extra careful so we don't delete production data + if (!["qa", "dev"].includes(appMode)) { + throw new Error(`Invalid appMode ${appMode}`); + } + + // Clean up Firestore and Realtime database roots that haven't been launched in `hoursAgo` hours + const cutOffMillis = Date.now() - hoursAgo*HOUR; + const qaRootsResult = await getFirestore() + .collection(appMode) + .where("lastLaunchTime", "<", Timestamp.fromMillis(cutOffMillis)) + .get(); + + logger.info(`Found ${qaRootsResult.size} roots to delete`); + + // Need to be careful to clean up the root in the realtime database + // first. The record in Firestore is our only way to figure out which + // roots in the realtime database need to be deleted. + for (const root of qaRootsResult.docs) { + // The Realtime database root is deleted first incase it fails. + // This way the root in firestore will remain so we can find it + // and try again later. + const databasePath = `/${appMode}/${root.id}`; + logger.info(`Deleting Realtime Database root: ${databasePath} ...`); + if (!dryRun) await getDatabase().ref(`/${appMode}/${root.id}`).remove(); + logger.info(`Deleting Firestore root: ${root.ref.path} ...`); + if (!dryRun) await getFirestore().recursiveDelete(root.ref); + } + +} diff --git a/functions/src/shared-utils.test.ts b/shared/shared-utils.test.ts similarity index 97% rename from functions/src/shared-utils.test.ts rename to shared/shared-utils.test.ts index acd4f95b74..f840efd814 100644 --- a/functions/src/shared-utils.test.ts +++ b/shared/shared-utils.test.ts @@ -35,16 +35,16 @@ describe("safeJsonParse", () => { it("should return parsed result with valid JSON", () => { const obj = { prop: "value" }; expect(safeJsonParse(JSON.stringify(obj))).toEqual(obj); - }) + }); it("should return undefined for invalid JSON", () => { expect(safeJsonParse()).toBeUndefined(); expect(safeJsonParse("{")).toBeUndefined(); - }) + }); }); describe("buildFirebaseImageUrl", () => { it("should work as expected", () => { - expect(buildFirebaseImageUrl("class-hash", "image-key")).toBe("ccimg://fbrtdb.concord.org/class-hash/image-key") + expect(buildFirebaseImageUrl("class-hash", "image-key")).toBe("ccimg://fbrtdb.concord.org/class-hash/image-key"); }); }); diff --git a/functions/src/shared-utils.ts b/shared/shared-utils.ts similarity index 100% rename from functions/src/shared-utils.ts rename to shared/shared-utils.ts diff --git a/functions/test/shared.test.ts b/shared/shared.test.ts similarity index 96% rename from functions/test/shared.test.ts rename to shared/shared.test.ts index 79b69d5abf..0a758162b6 100644 --- a/functions/test/shared.test.ts +++ b/shared/shared.test.ts @@ -1,11 +1,12 @@ -import { buildSectionPath, escapeKey, getCurriculumMetadata, isProblemPath, isSectionPath, networkDocumentKey, parseProblemPath, parseSectionPath } from "../src/shared"; +import { buildSectionPath, escapeKey, getCurriculumMetadata, isProblemPath, isSectionPath, + networkDocumentKey, parseProblemPath, parseSectionPath } from "./shared"; describe("shared types and utilities", () => { describe("escapeKey", () => { it("should escape the appropriate characters", () => { - expect(escapeKey(".$[]#\/")).toBe("______"); + expect(escapeKey(".$[]#/")).toBe("______"); const kNormalChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@%^&*()-_=+"; expect(escapeKey(kNormalChars)).toBe(kNormalChars); diff --git a/functions/src/shared.ts b/shared/shared.ts similarity index 94% rename from functions/src/shared.ts rename to shared/shared.ts index 642a8f3354..91eb35bc33 100644 --- a/functions/src/shared.ts +++ b/shared/shared.ts @@ -1,5 +1,5 @@ export const escapeKey = (s: string): string => { - return s.replace(/[.$[\]#\/]/g, "_"); + return s.replace(/[.$[\]#/]/g, "_"); }; const kProblemPathRegEx = /(.+)\/(\d)\/(\d)$/; @@ -19,7 +19,7 @@ export const isProblemPath = (key?: string) => { export const parseProblemPath = (key?: string) => { const result = kProblemPathRegEx.exec(key || ""); return result ? result?.slice(1) : undefined; -} +}; /* * isSectionPath @@ -45,12 +45,12 @@ export const isSectionPath = (key?: string) => { */ export const parseSectionPath = (key?: string) => { const result = kSectionPathRegEx.exec(key || ""); - return result ? [result[1], ...result?.slice(3)] : undefined; -} + return result ? [result[1], ...result.slice(3)] : undefined; +}; const facetMap: Record = { "teacher-guide": "guide" -} +}; export const buildProblemPath = (unitCode: string, investigationOrdinal: string, problemOrdinal: string) => { return `${unitCode}/${investigationOrdinal}/${problemOrdinal}`; @@ -70,7 +70,7 @@ export const getCurriculumMetadata = (sectionPath?: string): ICurriculumMetadata return sectionPath && unit && investigation && problem && section ? { unit, facet, problem: `${investigation}.${problem}`, section, path: sectionPath } : undefined; -} +}; /* * Types that are shared between cloud functions and client code. @@ -100,6 +100,12 @@ export function networkDocumentKey(uid: string, documentKey: string, network?: s return `${prefix}_${escapedKey}`; } +export function getDocumentPath(userId: string, documentKey: string, network?: string) { + const networkDocKey = networkDocumentKey(userId, documentKey, network); + const documentPath = `documents/${networkDocKey}`; + return documentPath; +} + export interface IDocumentMetadata { uid: string; type: string; @@ -108,6 +114,12 @@ export interface IDocumentMetadata { title?: string; originDoc?: string; properties?: Record; + tools?: string[]; + strategies?: string[]; + investigation?: string; + problem?: string; + unit?: string|null; + visibility?: string; } export function isDocumentMetadata(o: any): o is IDocumentMetadata { return !!o.uid && !!o.type && !!o.key; diff --git a/src/assets/workspace-instance-scroll.svg b/src/assets/workspace-instance-scroll.svg new file mode 100644 index 0000000000..d30754e9cf --- /dev/null +++ b/src/assets/workspace-instance-scroll.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/clue/clue.sass b/src/clue/clue.sass index c3a607eb5b..7b41f01e8a 100644 --- a/src/clue/clue.sass +++ b/src/clue/clue.sass @@ -53,6 +53,8 @@ body &:active, &.selected background-color: $support-blue-light-4 font-weight: bold +.tab-sort-work + background: $classwork-purple-light-4 .document .titlebar diff --git a/src/clue/data-colors.scss b/src/clue/data-colors.scss index 59eead90c8..15dc1fbf8a 100644 --- a/src/clue/data-colors.scss +++ b/src/clue/data-colors.scss @@ -1,4 +1,4 @@ -// These colors are used for data points in the Graph and Geometry tiles +// These colors are used for data points in the Graph, Bar Graph, and Geometry tiles // The export allows them to be referenced in Javascript (via data-colors.scss.d.ts) $data-blue: #0069ff; diff --git a/src/cms/cms-document-editor.tsx b/src/cms/cms-document-editor.tsx index 666de93e3d..fa9287d36d 100644 --- a/src/cms/cms-document-editor.tsx +++ b/src/cms/cms-document-editor.tsx @@ -22,7 +22,7 @@ interface IState { document?: DocumentModelType; } -const stores = initializeApp("dev", true); +const stores = initializeApp(true); export class CmsDocumentEditor extends React.Component { disposer: IDisposer; diff --git a/src/components/annotations/annotation-arrow.scss b/src/components/annotations/annotation-arrow.scss index b87f837079..8283be16ca 100644 --- a/src/components/annotations/annotation-arrow.scss +++ b/src/components/annotations/annotation-arrow.scss @@ -17,9 +17,14 @@ .arrow-stem { stroke-opacity: 0; stroke-width: 11px; + pointer-events: none; + + .annotation-layer.show-handles & { + pointer-events: visible; + } } - &:hover { + .annotation-layer.show-handles &:hover { .arrow-stem { stroke-opacity: .125; } diff --git a/src/components/annotations/annotation-button.scss b/src/components/annotations/annotation-button.scss index 144aacba78..a972ae2e32 100644 --- a/src/components/annotations/annotation-button.scss +++ b/src/components/annotations/annotation-button.scss @@ -6,7 +6,7 @@ fill: transparent; position: absolute; - &:hover { + .annotation-layer.show-buttons &:hover { fill: vars.$annotation-blue-very-transparent; stroke: vars.$annotation-blue; stroke-dasharray: 2; diff --git a/src/components/annotations/annotation-node.scss b/src/components/annotations/annotation-node.scss index 62613df72c..1b459211cc 100644 --- a/src/components/annotations/annotation-node.scss +++ b/src/components/annotations/annotation-node.scss @@ -9,7 +9,7 @@ fill: rgba(0, 0, 0, 0); } - &:hover { + .annotation-layer.show-handles &:hover { .node-highlight{ fill: vars.$annotation-blue-very-transparent; } @@ -28,4 +28,4 @@ fill: vars.$annotation-blue; } } -} \ No newline at end of file +} diff --git a/src/components/annotations/arrow-annotation.scss b/src/components/annotations/arrow-annotation.scss index 3dd44430a0..bc2c1ec07e 100644 --- a/src/components/annotations/arrow-annotation.scss +++ b/src/components/annotations/arrow-annotation.scss @@ -1,119 +1,147 @@ @use "../vars.sass"; -.text-object { - pointer-events: none; - - .text-region { - align-items: center; - display: flex; - height: 100%; - justify-content: center; - pointer-events: none; - width: 100%; - - .text-box { - background-color: white; - border: 2px solid vars.$annotation-blue; - border-radius: 10px; - max-width: calc(100% - 8px); - - &.text-display { - color: vars.$annotation-blue; +.annotation-layer { - &.default-text { - font-style: italic; - } + .text-object { + pointer-events: none; - &:hover { - outline: 4px solid vars.$annotation-blue-very-transparent; - } + .text-region { + align-items: center; + display: flex; + height: 100%; + justify-content: center; + pointer-events: none; + width: 100%; + + .text-box { + background-color: white; + border: 2px solid vars.$annotation-blue; + border-radius: 10px; + max-width: calc(100% - 8px); + + &.text-display { + color: vars.$annotation-blue; + + &.default-text { + font-style: italic; + } - &.can-edit { &:hover { - cursor: pointer; + outline: 4px solid vars.$annotation-blue-very-transparent; } - } - &.dragging { - background-color: vars.$annotation-blue; - color: white; + &.can-edit { + &:hover { + cursor: pointer; + } + } - &:hover { - outline: 4px solid vars.$annotation-blue-transparent; + &.dragging { + background-color: vars.$annotation-blue; + color: white; + + &:hover { + outline: 4px solid vars.$annotation-blue-transparent; + } } } - } - &.text-input { - background-color: vars.$annotation-light-blue; - text-align: center; + &.text-input { + background-color: vars.$annotation-light-blue; + text-align: center; + } } } } -} - -.drag-handle { - cursor: pointer; -} - -.sparrow-delete-button { - cursor: pointer; - fill-opacity: 0; - .sparrow-delete-button-front { - // Allow hover and click to pass through to background 'highlight' element. + // Only allow interacting with the drag handle when we see the 'show-handles' class. + .drag-handle { pointer-events: none; } - .sparrow-delete-icon { - // Allow hover and click to pass through to background 'highlight' element. - pointer-events: none; - } - - .sparrow-delete-button-highlight { - fill: vars.$annotation-blue; + &.show-handles .drag-handle { + cursor: pointer; + pointer-events: auto; } - .actual-sparrow.selected & { - .sparrow-delete-button-highlight { - fill-opacity: 0; - - &:hover, .sparrow-delete-button:hover & { - fill-opacity: .125; - } - } + .sparrow-delete-button { + fill-opacity: 0; + pointer-events: none; .sparrow-delete-button-front { - fill: vars.$annotation-blue; - fill-opacity: 1; + // Allow hover and click to pass through to background 'highlight' element. + pointer-events: none; } + .sparrow-delete-icon { - fill-opacity: 1; + // Allow hover and click to pass through to background 'highlight' element. + pointer-events: none; } - } - &:hover { .sparrow-delete-button-highlight { - fill-opacity: .125; + fill: vars.$annotation-blue; } - .sparrow-delete-button-front { - fill: vars.$annotation-blue; - fill-opacity: 1; + .actual-sparrow.selected & { + .sparrow-delete-button-highlight { + fill-opacity: 0; + + &:hover, + .sparrow-delete-button:hover & { + fill-opacity: .125; + } + } + + .sparrow-delete-button-front { + fill: vars.$annotation-blue; + fill-opacity: 1; + } + + .sparrow-delete-icon { + fill-opacity: 1; + } } - .sparrow-delete-icon { - fill-opacity: 1; + &:active { + .sparrow-delete-button-highlight { + fill-opacity: .25; + } + + .sparrow-delete-button-front { + fill: vars.$annotation-blue; + fill-opacity: 1; + } + + .sparrow-delete-icon { + fill-opacity: 1; + } } } - &:active { - .sparrow-delete-button-highlight { - fill-opacity: .25; + &.show-handles .sparrow-delete-button { + cursor: pointer; + pointer-events: auto; + + &:hover { + .sparrow-delete-button-highlight { + fill-opacity: .125; + } + + .sparrow-delete-button-front { + fill: vars.$annotation-blue; + fill-opacity: 1; + } + + .sparrow-delete-icon { + fill-opacity: 1; + } } + } + + &.show-handles .visible-delete-button { .sparrow-delete-button-front { - fill: vars.$annotation-blue; + fill: vars.$annotation-half-blue; fill-opacity: 1; } @@ -122,14 +150,3 @@ } } } - -.visible-delete-button { - .sparrow-delete-button-front { - fill: vars.$annotation-half-blue; - fill-opacity: 1; - } - - .sparrow-delete-icon { - fill-opacity: 1; - } -} diff --git a/src/components/annotations/arrow-annotation.tsx b/src/components/annotations/arrow-annotation.tsx index b347bdf9f3..bbd228ff15 100644 --- a/src/components/annotations/arrow-annotation.tsx +++ b/src/components/annotations/arrow-annotation.tsx @@ -72,6 +72,8 @@ interface IArrowAnnotationProps { canEdit?: boolean; deleteArrow: (arrowId: string) => void; handleArrowClick: (arrowId: string, event: React.MouseEvent) => void; + handleDragHandleNonDrag: + (e: MouseEvent, tileId?: string, objectId?: string, objectType?: string) => void; documentBottom: number; documentLeft: number; documentRight: number; @@ -84,7 +86,7 @@ interface IArrowAnnotationProps { } export const ArrowAnnotationComponent = observer( function ArrowAnnotationComponent({ - arrow, canEdit, deleteArrow, handleArrowClick, + arrow, canEdit, deleteArrow, handleArrowClick, handleDragHandleNonDrag, documentBottom, documentLeft, documentRight, documentTop, getBoundingBox, getObjectNodeRadii, readOnly }: IArrowAnnotationProps) { @@ -111,6 +113,7 @@ export const ArrowAnnotationComponent = observer( const [dragType, setDragType] = useState(); const [dragX, setDragX] = useState(); const [dragY, setDragY] = useState(); + const mouseDownTime = useRef(); const dragging = clientX !== undefined && clientY !== undefined && dragX !== undefined && dragY !== undefined; const draggingSource = dragging && dragType === "source"; const draggingTarget = dragging && dragType === "target"; @@ -185,13 +188,16 @@ export const ArrowAnnotationComponent = observer( } } - // Set up drag handles + // Set up drag handles. + // A quick click that doesn't move on a drag handle doesn't drag it; it creates a new Sparrow. + // So, mouse down tracks the time and location. function handleMouseDown(e: React.MouseEvent, _dragType: DragType) { if (!canEdit) return; setDragX(e.clientX); setDragY(e.clientY); setDragType(_dragType); + mouseDownTime.current = performance.now(); function handleMouseMove(e2: MouseEvent) { setClientX(e2.clientX); @@ -212,18 +218,30 @@ export const ArrowAnnotationComponent = observer( const dy = Math.max(textMinYOffset ?? 0, Math.min(textMaxYOffset ?? 0, startingDy + dDy)); setFunc(dx, dy); } else { - // For source and target changes, also update the text offset propoprtionally - const currentDragOffsets = determineDragOffsets(_dragType, e2.clientX, e2.clientY, e.clientX, e.clientY); - const { textCenterX: tcX, textCenterY: tcY, textOriginX: toX, textOriginY: toY } - = arrow.getPoints(documentLeft, documentRight, documentTop, documentBottom, - currentDragOffsets, sourceBB, targetBB); - if (tcX !== undefined && tcY !== undefined) { - arrow.setTextOffset(tcX - toX, tcY - toY); + if (mouseDownTime.current + && performance.now() - mouseDownTime.current < 500 + && Math.abs(dDx) < 4 + && Math.abs(dDy) < 4) { + // If the mouse didn't move much and the duration was short, treat it as a click + const attachedObject = _dragType === "source" ? arrow.sourceObject : arrow.targetObject; + if (attachedObject) { + handleDragHandleNonDrag(e2, attachedObject.tileId, attachedObject.objectId, attachedObject.objectType); + } else { + handleDragHandleNonDrag(e2); // free end of arrow has no object. + } + } else { + // For source and target changes, also update the text offset propoprtionally + const currentDragOffsets = determineDragOffsets(_dragType, e2.clientX, e2.clientY, e.clientX, e.clientY); + const { textCenterX: tcX, textCenterY: tcY, textOriginX: toX, textOriginY: toY } + = arrow.getPoints(documentLeft, documentRight, documentTop, documentBottom, + currentDragOffsets, sourceBB, targetBB); + if (tcX !== undefined && tcY !== undefined) { + arrow.setTextOffset(tcX - toX, tcY - toY); + } + // And then update the source or target + setFunc(boundDelta(startingDx + dDx, widthBound), boundDelta(startingDy + dDy, heightBound)); } - // And then update the source or target - setFunc(boundDelta(startingDx + dDx, widthBound), boundDelta(startingDy + dDy, heightBound)); } - setClientX(undefined); setClientY(undefined); setDragX(undefined); diff --git a/src/components/app.tsx b/src/components/app.tsx index 9a518c7c8d..8c803fe637 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -17,15 +17,11 @@ import "react-tippy/dist/tippy.css"; import "./app.scss"; interface IProps extends IBaseProps {} -interface IState { - qaCleared: boolean; - qaClearError?: string; -} function resolveAppMode( stores: IStores, - rawFirebaseJWT: string | undefined, - onQAClear?: (result: boolean, err?: string) => void) { + rawFirebaseJWT: string | undefined +) { const { appMode, db, ui} = stores; if (appMode === "authed") { if (rawFirebaseJWT) { @@ -39,18 +35,8 @@ function resolveAppMode( return db.connect({appMode, stores}) .then(() => { if (appMode === "qa") { - const {qaClear, qaGroup} = urlParams; - if (qaClear) { - const cleared = (err?: string) => { - if (onQAClear) { - onQAClear(!err, err); - } - }; - db.clear(qaClear) - .then(() => cleared()) - .catch(cleared); - } - else if (qaGroup) { + const {qaGroup} = urlParams; + if (qaGroup) { db.leaveGroup().then(() => db.joinGroup(qaGroup)); } } @@ -61,7 +47,7 @@ function resolveAppMode( } } -export const authAndConnect = (stores: IStores, onQAClear?: (result: boolean, err?: string) => void) => { +export const authAndConnect = (stores: IStores) => { const {appConfig, curriculumConfig, appMode, db, user, ui} = stores; let rawPortalJWT: string | undefined; @@ -118,7 +104,7 @@ export const authAndConnect = (stores: IStores, onQAClear?: (result: boolean, er stores.loadUnitAndProblem(unitCode, problemId); } } - return resolveAppMode(stores, authenticatedUser.rawFirebaseJWT, onQAClear); + return resolveAppMode(stores, authenticatedUser.rawFirebaseJWT); }) .then(() => { return user.isTeacher @@ -128,11 +114,8 @@ export const authAndConnect = (stores: IStores, onQAClear?: (result: boolean, er .then(firestoreUser => { if (firestoreUser?.network) { user.setNetworks(firestoreUser.network, firestoreUser.networks); - - if (rawPortalJWT) { - syncTeacherClassesAndOfferings(db.firestore, user, rawPortalJWT); - } } + syncTeacherClassesAndOfferings(db.firestore, user, stores.class, rawPortalJWT); }) .then(() => { removeLoadingMessage("Connecting"); @@ -151,19 +134,12 @@ export const authAndConnect = (stores: IStores, onQAClear?: (result: boolean, er @inject("stores") @observer -export class AppComponent extends BaseComponent { - - public state: IState = { - qaCleared: false, - qaClearError: undefined - }; +export class AppComponent extends BaseComponent { constructor(props: IProps) { super(props); - authAndConnect(this.stores, (qaCleared, qaClearError) => { - this.setState({qaCleared, qaClearError}); - }); + authAndConnect(this.stores); } public componentWillUnmount() { @@ -200,15 +176,6 @@ export class AppComponent extends BaseComponent { return this.renderApp(this.renderLoading()); } - if (urlParams.qaClear) { - const {qaCleared, qaClearError} = this.state; - return this.renderApp( - - {qaCleared ? `QA Cleared: ${qaClearError || "OK"}` : "QA Clearing..."} - - ); - } - if (user.isStudent) { if (!user.currentGroupId) { if (appConfig.autoAssignStudentsToIndividualGroups || this.stores.isPreviewing) { diff --git a/src/components/chat/commented-documents.tsx b/src/components/chat/commented-documents.tsx index b7840e544a..ae786465d4 100644 --- a/src/components/chat/commented-documents.tsx +++ b/src/components/chat/commented-documents.tsx @@ -1,13 +1,13 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; +import { observer } from "mobx-react"; import { useFirestore } from "../../hooks/firestore-hooks"; import { useStores, usePersistentUIStore, useUserStore, useUIStore} from "../../hooks/use-stores"; import { useDocumentCaption } from "../../hooks/use-document-caption"; -import { CurriculumDocument, DocumentDocument } from "../../lib/firestore-schema"; -import { getSectionTitle } from "../../models/curriculum/section"; import { UserModelType } from "../../models/stores/user"; import { getNavTabOfDocument, getTabsOfCurriculumDoc, isStudentWorkspaceDoc } from "../../models/stores/persistent-ui"; import { DocumentModelType } from "../../models/document/document"; +import { CommentedDocumentsQuery } from "../../models/commented-documents"; import DocumentIcon from "../../assets/icons/document-icon.svg"; @@ -17,18 +17,9 @@ interface IProps { user?: UserModelType handleDocView: (() => void) | undefined; } -interface PromisedCurriculumDocument extends CurriculumDocument { - id?: string, - title?: string, - numComments?: number -} -export interface PromisedDocumentDocument extends DocumentDocument { - id?: string, - numComments?: number, - title?: string -} -export const CommentedDocuments: React.FC = ({user, handleDocView}) => { +export const CommentedDocuments: React.FC + = observer(function CommentedDocuments({user, handleDocView}) { const [db] = useFirestore(); const ui = useUIStore(); const persistentUI = usePersistentUIStore(); @@ -36,109 +27,19 @@ export const CommentedDocuments: React.FC = ({user, handleDocView}) => { const problem = store.problemOrdinal; const unit = store.unit.code; - //------Curriculum Documents: (i.e. //"Problem"/"Teacher-Guide") - const [docsCommentedOn, setDocsCommentedOn] = useState(); - const cDocsRef = useMemo(() => db.collection("curriculum"), [db]); - const cDocsInScopeRef = useMemo(() => { - if (user?.network){ - return cDocsRef - .where("unit", "==", unit) - .where("problem", "==", problem) - .where("network","==", user?.network); - } else { - return cDocsRef - .where("unit", "==", unit) - .where("problem", "==", problem) - //for teachers not in network, look for documents matching the uid - .where ("uid", "==", user?.id); - } - }, [cDocsRef, problem, unit, user?.network, user?.id]); + const [commentedDocumentsQuery] + = useState(() => new CommentedDocumentsQuery(db, unit, problem)); - //------Curriculum Documents: (i.e. //"Problem"/"Teacher-Guide") useEffect(() => { - const unsubscribeFromDocs = cDocsInScopeRef.onSnapshot(querySnapshot => { - const docs = querySnapshot.docs.map(doc => { - return ( - { - id: doc.id, - title: "temp", - numComments: 0, - ...doc.data() - } - ); - }); - const commentedDocs: PromisedCurriculumDocument[] = []; - const promiseArr: Promise[] = []; - for (let doc of docs){ - const docCommentsRef = cDocsRef.doc(doc.id).collection("comments"); - promiseArr.push(docCommentsRef.get().then((qs) => { - if (qs.empty === false) { - const firstCharPosition = doc.id.split("_", 4).join("_").length + 1; //first char after 4th _ - const sectionType = doc.id.substring(firstCharPosition, doc.id.length); - doc = {...doc, title: getSectionTitle(sectionType), numComments: qs.size}; - commentedDocs.push(doc as PromisedCurriculumDocument); - } - })); - } - Promise.all(promiseArr).then((results)=>{ - setDocsCommentedOn(commentedDocs); - }); - }); - return () => unsubscribeFromDocs?.(); - },[cDocsRef, cDocsInScopeRef]); - - - //------Documents: (i.e. //"Student Workspaces/"My Work"/"Class Work") - const [workDocuments, setWorkDocuments] = useState(); - const mDocsRef = useMemo(() => db.collection("documents"), [db]); - const mDocsInScopeRef = useMemo(() => { - if(user?.network){ - return mDocsRef.where("network", "==", user?.network); - } else { - return mDocsRef.where("uid", "==", user?.id); + if (user) { + commentedDocumentsQuery.setUser(user); } - }, [mDocsRef, user?.network, user?.id]); - - //------Documents: (i.e. //"Student Workspaces/"My Work"/"Class Work") - useEffect(() => { - const unsubscribeFromDocs = mDocsInScopeRef.onSnapshot(querySnapshot=>{ - const docs = querySnapshot.docs.map(doc =>{ //convert each element of docs to an object - return ( - { - id: doc.id, - type: doc.data().type, - numComments: 0, - title: "temp", - key: doc.data().key, - ...doc.data() - } - ); - }); - const commentedDocs: PromisedDocumentDocument[]= []; - const promiseArr: Promise[]=[]; - for (let doc of docs){ - const docCommentsRef = mDocsRef.doc(doc.id).collection("comments"); - promiseArr.push(docCommentsRef.get().then((qs)=>{ - if (qs.empty === false){ - doc = {...doc, numComments: qs.size}; - commentedDocs.push(doc as PromisedDocumentDocument); - } - })); - } - - Promise.all(promiseArr).then(()=>{ - setWorkDocuments(commentedDocs); - }); - - }); - return () => unsubscribeFromDocs?.(); - },[mDocsRef, mDocsInScopeRef]); + }, [commentedDocumentsQuery, user]); return (
{ - docsCommentedOn && - (docsCommentedOn).map((doc: PromisedCurriculumDocument, index:number) => { //Problem + Teacher Guide documents + (commentedDocumentsQuery.curriculumDocs).map((doc, index) => { const {navTab} = getTabsOfCurriculumDoc(doc.path); return (
= ({user, handleDocView}) => { }) } { - workDocuments && - (workDocuments).map((doc: PromisedDocumentDocument, index: number) =>{ - //"Student Workspaces/"My Work"/"Class Work" + (commentedDocumentsQuery.userDocs).map((doc, index) =>{ const sectionDoc = store.documents.getDocument(doc.key); const networkDoc = store.networkDocuments.getDocument(doc.key); if (sectionDoc){ @@ -197,7 +96,7 @@ export const CommentedDocuments: React.FC = ({user, handleDocView}) => { }
); -}; +}); interface JProps { diff --git a/src/components/document/annotation-layer.tsx b/src/components/document/annotation-layer.tsx index 3b048bcf6e..59f927e943 100644 --- a/src/components/document/annotation-layer.tsx +++ b/src/components/document/annotation-layer.tsx @@ -51,6 +51,11 @@ export const AnnotationLayer = observer(function AnnotationLayer({ const hotKeys = useMemoOne(() => new HotKeys(), []); const shape: ArrowShape = isArrowShape(ui.annotationMode) ? ui.annotationMode : ArrowShape.curved; + // Buttons are active unless a straight sparrow is being drawn from an object + const showButtons = !(shape === ArrowShape.straight && sourceObjectId); + // Drag handles are active unless any sort of sparrow is being drawn + const showDragHandles = !(sourceObjectId || sourcePoint); + useEffect(() => { const deleteSelected = () => content?.deleteSelected(); if (!readOnly) { @@ -132,7 +137,7 @@ export const AnnotationLayer = observer(function AnnotationLayer({ setIsBackgroundClick(isBackground); }; - const handleMouseMove: MouseEventHandler = event => { + const handleMouseMove = (event: { clientX: number, clientY: number }) => { if (divRef.current) { const bb = divRef.current.getBoundingClientRect(); setMouseX(event.clientX - bb.left); @@ -319,10 +324,35 @@ export const AnnotationLayer = observer(function AnnotationLayer({ content?.selectAnnotations([]); }; + /** + * Handle the case where a drag handle is clicked. + * We treat a long-press or drag as an intention to move the handle, + * but a quick click as an intention to create a new arrow. + */ + const handleDragHandleNonDrag = (e: globalThis.MouseEvent, + tileId?: string, objectId?: string, objectType?: string) => { + // Verify that there is no source object + if (sourceObjectId || sourcePoint) return; + + if (tileId && objectId) { + // Set the source object to the clicked handle's object + setSourceTileId(tileId); + setSourceObjectId(objectId); + setSourceObjectType(objectType); + } else { + if (shape === ArrowShape.straight) { + // Must have clicked the free end of a straight arrow, which has no object. + // Assuming we're in straight-arrow mode, start a new arrow with the free end here. + handleMouseMove(e); + setSourcePoint([mouseX ?? 0, mouseY ?? 0]); + } + } + }; + const handleAnnotationButtonClick = (e: React.MouseEvent, tileId: string, objectId: string, objectType?: string) => { // If we are in straight arrow mode, and one object has already been // selected, then we ignore the object clicked on and create an arrow to this X,Y location. - if (shape === ArrowShape.straight && sourceObjectId) { + if (!showButtons) { createAnnotation(); clearSource(); return; @@ -355,7 +385,8 @@ export const AnnotationLayer = observer(function AnnotationLayer({ const rowIds = content?.rowOrder || []; const editing = ui.annotationMode !== undefined; const hidden = !persistentUI.showAnnotations; - const classes = classNames("annotation-layer", { editing, hidden }); + const classes = classNames("annotation-layer", + { editing, hidden, 'show-buttons': showButtons, 'show-handles': showDragHandles }); return (
content?.deleteAnnotation(arrowId)} handleArrowClick={handleArrowClick} + handleDragHandleNonDrag={handleDragHandleNonDrag} documentBottom={documentBottom} documentLeft={documentLeft} documentRight={documentRight} diff --git a/src/components/document/doc-list-debug.tsx b/src/components/document/doc-list-debug.tsx index 85c8b9d13e..1bdd1c068d 100644 --- a/src/components/document/doc-list-debug.tsx +++ b/src/components/document/doc-list-debug.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { DocumentModelType } from "../../models/document/document"; +import { IDocumentMetadata } from "../../../shared/shared"; import "./doc-list-debug.scss"; interface IProps { - docs: DocumentModelType[]; + docs: IDocumentMetadata[]; } export function DocListDebug(props: IProps) { @@ -30,9 +30,10 @@ export function DocListDebug(props: IProps) { {ct} {doc.key} {doc.type} - {doc.visibility ? doc.visibility : "undefined"} + {/* TODO: Reinstate visibility and groupId */} + {/* {doc.visibility ? doc.visibility : "undefined"} */} {doc.uid} - {doc.groupId ?? " "} + {/* {doc.groupId ?? " "} */} {doc.title} ); diff --git a/src/components/document/document-file-menu.tsx b/src/components/document/document-file-menu.tsx index 32665719fa..6b0e8bf22c 100644 --- a/src/components/document/document-file-menu.tsx +++ b/src/components/document/document-file-menu.tsx @@ -35,7 +35,7 @@ function showPublishOption(document: DocumentModelType, stores: IStores) { if (document.type === "planning" || appConfig.disablePublish === true) return false; return appConfig.disablePublish .findIndex(spec => { - return stores.sortedDocuments.isMatchingSpec(document, spec.documentType, spec.properties); + return stores.sectionDocuments.isMatchingSpec(document, spec.documentType, spec.properties); }) < 0; } diff --git a/src/components/document/document-group.scss b/src/components/document/document-group.scss new file mode 100644 index 0000000000..f435f25183 --- /dev/null +++ b/src/components/document/document-group.scss @@ -0,0 +1,112 @@ +@import "../vars"; + +.doc-group { + align-items: center; + background: $classwork-purple-light-9; + display: flex; + margin-bottom: 2px; + padding: 4px 10px; + width: 100%; + + .doc-group-label { + align-items: center; + display: flex; + flex-basis: 140px; + flex-grow: 0; + flex-shrink: 0; + font-size: 13px; + font-weight: normal; + margin-right: 10px; + + svg { + margin-right: 5px; + } + } + .doc-group-list { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow: hidden; + width: 100%; + + &.simple { + align-items: flex-start; + column-gap: 10px; + display: flex; + flex-grow: 1; + row-gap: 10px; + } + } + .doc-group-count { + flex-basis: 60px; + flex-grow: 0; + flex-shrink: 0; + font-size: 13px; + font-weight: normal; + text-align: right; + } +} + +.scroll-button { + background: #fff; + border: solid 1px #707070; + border-radius: 1px; + color: $classwork-purple-dark-1; + cursor: pointer; + display: flex; + flex-grow: 0; + flex-shrink: 0; + height: 16px; + justify-content: center; + line-height: 1; + margin: 0; + padding: 0; + width: 16px; + + &.scroll-left { + margin-right: 10px; + } + &.scroll-right { + margin-left: 10px; + + svg { + transform: rotate(180deg); + } + } + + svg { + margin: -3px; + } + + &:hover { + svg { + rect { + fill: $classwork-purple-light-4; + } + } + } + + &:active { + svg { + path { + fill: white; + } + rect { + fill: $classwork-purple-dark-1; + } + } + } + &:disabled { + cursor: not-allowed; + opacity: 0.35; + + svg { + path { + fill: $classwork-purple-dark-1; + } + rect { + fill: white; + } + } + } +} diff --git a/src/components/document/document-group.tsx b/src/components/document/document-group.tsx new file mode 100644 index 0000000000..ef65809054 --- /dev/null +++ b/src/components/document/document-group.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { DocumentModelType, getDocumentContext } from "../../models/document/document"; +import { SimpleDocumentItem } from "../thumbnail/simple-document-item"; +import { DocumentContextReact } from "./document-context"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { DocumentGroup } from "../../models/stores/document-group"; + +import ScrollArrowIcon from "../../assets/workspace-instance-scroll.svg"; + +import "./document-group.scss"; + +interface IProps { + documentGroup: DocumentGroup; + secondarySort: string; + onSelectDocument: (document: DocumentModelType | IDocumentMetadata) => void; +} + +export const DocumentGroupComponent = observer(function DocumentGroupComponent(props: IProps) { + const { documentGroup, secondarySort, onSelectDocument } = props; + const docBoxWidth = 16; + const docBoxGap = 10; + const scrollUnit = docBoxWidth + docBoxGap; + const docCount = documentGroup.documents.length || 0; + const isUnsorted = secondarySort === "None"; + const docListContainerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + const [visibleCount, setVisibleCount] = useState(0); + const [leftArrowDisabled, setLeftArrowDisabled] = useState(true); + const [rightArrowDisabled, setRightArrowDisabled] = useState(false); + + // Each document in the group is represented by a square box. The group of document boxes is displayed in + // a single row. If there are more boxes than can fit within the row's width, scroll buttons are added + // to either side of the list so the user can scroll through it. + const handleScroll = (direction: "left" | "right") => { + if (docListContainerRef.current) { + const scrollAmount = visibleCount * scrollUnit; + docListContainerRef.current.scrollBy({ + left: direction === "left" ? -scrollAmount : scrollAmount, + behavior: "smooth" + }); + } + }; + + // Set up a resize observer for responding to changes to the document list container's width. + useEffect(() => { + const docListContainer = docListContainerRef.current; + + const updateWidth = () => { + if (docListContainer) { + setContainerWidth(docListContainer.offsetWidth); + } + }; + + updateWidth(); + + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); + + if (docListContainer) { + resizeObserver.observe(docListContainer); + } + + return () => { + if (docListContainer) { + resizeObserver.unobserve(docListContainer); + } + }; + }, []); + + // Calculate the number of visible documents based on the current container width + useEffect(() => { + if (docListContainerRef.current) { + const count = Math.floor(containerWidth / scrollUnit); + setVisibleCount(count); + } + }, [containerWidth, scrollUnit]); + + // Update arrow button states based on scroll position. + useEffect(() => { + const updateArrowStates = () => { + if (docListContainerRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = docListContainerRef.current; + setLeftArrowDisabled(scrollLeft === 0); + setRightArrowDisabled(scrollLeft + clientWidth >= scrollWidth); + } + }; + + const docListContainer = docListContainerRef.current; + if (docListContainer) { + updateArrowStates(); + docListContainer.addEventListener("scroll", updateArrowStates); + + return () => { + docListContainer.removeEventListener("scroll", updateArrowStates); + }; + } + }, [visibleCount, scrollUnit]); + + const renderScrollButton = (direction: "left" | "right", disabled: boolean) => { + return ( + + ); + }; + + return ( +
+ {!isUnsorted && +
+ {documentGroup.icon ? : null}{documentGroup.label} +
+ } + {visibleCount < docCount && renderScrollButton("left", leftArrowDisabled)} +
+ {documentGroup.documents?.map((doc: any) => { + const documentContext = getDocumentContext(doc); + return ( + + + + ); + })} +
+ {visibleCount < docCount && renderScrollButton("right", rightArrowDisabled)} + {!isUnsorted &&
{docCount}
} +
+ ); +}); diff --git a/src/components/document/editable-document-content.tsx b/src/components/document/editable-document-content.tsx index 26b87e638a..5b2ec0dd98 100644 --- a/src/components/document/editable-document-content.tsx +++ b/src/components/document/editable-document-content.tsx @@ -101,7 +101,7 @@ export function EditableDocumentContent({ className, contained, mode, isPrimary, document, toolbar, readOnly, showPlayback, fullHeight }: IProps) { const documentContext = useDocumentContext(document); - const { db: { firebase }, ui, persistentUI, user } = useStores(); + const { db: { firebase, firestore }, ui, persistentUI, user } = useStores(); // set by the canvas and used by the toolbar const editableTileApiInterfaceRef: EditableTileApiInterfaceRef = useRef(null); const isReadOnly = !isPrimary || readOnly || document.isPublished; @@ -114,7 +114,7 @@ export function EditableDocumentContent({ contained ? "contained-editable-document-content" : "full-screen-editable-document-content", {"comment-select" : documentSelectedForComment, "full-height": fullHeight}, className); - useDocumentSyncToFirebase(user, firebase, document, readOnly); + useDocumentSyncToFirebase(user, firebase, firestore, document, readOnly); return ( diff --git a/src/components/document/sort-work-document-area.tsx b/src/components/document/sort-work-document-area.tsx index de36dd6b96..c4075ccd0b 100644 --- a/src/components/document/sort-work-document-area.tsx +++ b/src/components/document/sort-work-document-area.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import classNames from "classnames"; import { observer } from "mobx-react"; import { useAppConfig, useProblemStore, @@ -9,6 +9,7 @@ import { getDocumentDisplayTitle } from "../../models/document/document-utils"; import { ENavTab } from "../../models/view/nav-tabs"; import { isExemplarType } from "../../models/document/document-types"; import { ExemplarVisibilityCheckbox } from "./exemplar-visibility-checkbox"; +import { DocumentLoadingSpinner } from "./document-loading-spinner"; import EditIcon from "../../clue/assets/icons/edit-right-icon.svg"; import CloseIcon from "../../../src/assets/icons/close/close.svg"; @@ -25,14 +26,13 @@ export const SortWorkDocumentArea: React.FC = observer(function SortWork const classStore = useClassStore(); const problemStore = useProblemStore(); const appConfigStore = useAppConfig(); - const openDocument = store.documents.getDocument(openDocumentKey) || - store.networkDocuments.getDocument(openDocumentKey); + const [openDocument, setOpenDocument] = useState(); const isVisible = openDocument?.isAccessibleToUser(user, store.documents); const showPlayback = user.type && appConfigStore.enableHistoryRoles.includes(user.type); const showExemplarShare = user.type === "teacher" && openDocument && isExemplarType(openDocument.type); const getDisplayTitle = (document: DocumentModelType) => { const documentOwner = classStore.users.get(document.uid); - const documentTitle = getDocumentDisplayTitle(document, appConfigStore, problemStore); + const documentTitle = getDocumentDisplayTitle(document, appConfigStore, problemStore, store.unit.code); return {owner: documentOwner ? documentOwner.fullName : "", title: documentTitle}; }; const displayTitle = openDocument && getDisplayTitle(openDocument); @@ -66,6 +66,23 @@ export const SortWorkDocumentArea: React.FC = observer(function SortWork const sideClasses = { secondary: false, primary: false && !false }; + useEffect(() => { + const openDoc = store.documents.getDocument(openDocumentKey) || + store.networkDocuments.getDocument(openDocumentKey); + if (openDoc) { + setOpenDocument(openDoc); + return; + } + + const fetchOpenDoc = store.sortedDocuments.fetchFullDocument(openDocumentKey); + fetchOpenDoc.then((doc) => { + setOpenDocument(doc); + }); + + // TODO: Figure out how to cancel fetch if the component is unmounted before the fetch is complete + + }, [openDocumentKey, store.documents, store.networkDocuments, store.sortedDocuments]); + return (
= observer(function SortWork

This document is not shared with you right now.

} + { + !openDocument && + + }
); }); diff --git a/src/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index 0e978f1b27..eeca42f058 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -7,62 +7,9 @@ $title-margin: 2px; display: flex; flex-direction: column; - .sorted-sections{ - width: 100%; - - .section-header{ - height: 30px; - position: relative; - margin-top: 5px; - margin-bottom: 5px; - - &::after{ //divider line drawn across - content: ""; - position: absolute; - left: 0px; - right: 0px; - bottom: 50%; - border-bottom: 1px solid $charcoal-light-1; - } - - .section-header-label{ - svg{ - margin-right: 5px; - } - position: absolute; - left: 10px; - height: 26px; - width: calc(100% - 20px); - border-radius: 5px 5px 0px 0px; - background-color: $classwork-purple-light-7; - display: flex; - align-items: center; - justify-content: space-between; - color: $charcoal-dark-2; - z-index: 1; - padding: 0px 7px; - - .section-header-left { - display: flex; - align-items: center; - font-weight: bold; - } - - .section-header-right { - display: flex; - align-items: center; - - .section-header-arrow { - cursor: pointer; - fill: $classwork-purple-dark-1; - margin-right: 0px; - &.up { - transform: rotate(180deg); - } - } - } - } - } + .tab-panel-documents-section { + border: solid 1.5px $classwork-purple; + border-top: none; } .focus-document { diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 5acf62da29..a5716f2ba8 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -7,71 +7,103 @@ import { DEBUG_DOC_LIST } from "../../lib/debug"; import { SortWorkDocumentArea } from "./sort-work-document-area"; import { ENavTab } from "../../models/view/nav-tabs"; import { DocListDebug } from "./doc-list-debug"; -import { SortedDocuments } from "./sorted-documents"; +import { DocFilterType, PrimarySortType, SecondarySortType } from "../../models/stores/ui-types"; +import { SortedSection } from "./sorted-section"; +import { DocumentGroup } from "../../models/stores/document-group"; + import "../thumbnail/document-type-collection.scss"; +import "./sort-work-view.scss"; + /** * Resources pane view of class work and exemplars. * Various options for sorting the display are available - by user, by group, by tools used, etc. */ export const SortWorkView: React.FC = observer(function SortWorkView() { - const { appConfig, persistentUI, sortedDocuments } = useStores(); - - //*************************** Determine Sort Options & State *********************************** - const {tagPrompt} = appConfig; + const { appConfig, investigation, persistentUI, problem, sortedDocuments, unit } = useStores(); + const { tagPrompt } = appConfig; + const { docFilter: persistentUIDocFilter } = persistentUI; const sortTagPrompt = tagPrompt || ""; //first dropdown choice for comment tags const sortOptions = ["Group", "Name", sortTagPrompt, "Bookmarked", "Tools"]; - const [sortBy, setSortBy] = useState("Group"); + const filterOptions: DocFilterType[] = ["Problem", "Investigation", "Unit", "All"]; + const [primarySortBy, setPrimarySortBy] = useState("Group"); + const [secondarySortBy, setSecondarySortBy] = useState("None"); + const docFilter = persistentUIDocFilter; - useEffect(()=>{ - if (sortBy === sortTagPrompt){ - sortedDocuments.updateTagDocumentMap(); + const handleDocFilterSelection = (filter: DocFilterType) => { + persistentUI.setDocFilter(filter); + }; + + const handlePrimarySortBySelection = (sort: string) => { + setPrimarySortBy(sort); + if (sort === secondarySortBy) { + setSecondarySortBy("None"); } - },[sortedDocuments, sortBy, sortTagPrompt]); + }; - const sortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + const primarySortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + disabled: false, + selected: option === primarySortBy, text: option, - onClick: () => setSortBy(option) + onClick: () => handlePrimarySortBySelection(option) })); - let renderedSortedDocuments; - switch (sortBy) { - case "Group": - renderedSortedDocuments = sortedDocuments.sortByGroup; - break; - case "Name": - renderedSortedDocuments = sortedDocuments.sortByName; - break; - case sortTagPrompt: //Sort by Strategy - renderedSortedDocuments = sortedDocuments.sortByStrategy; - break; - case "Bookmarked": - renderedSortedDocuments = sortedDocuments.sortByBookmarks; - break; - case "Tools": - renderedSortedDocuments = sortedDocuments.sortByTools; - break; - } + const secondarySortOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + disabled: option === primarySortBy, + selected: option === secondarySortBy, + text: option, + onClick: () => setSecondarySortBy(option) + })); + secondarySortOptions.unshift({ + disabled: false, + selected: secondarySortBy === "None", + text: "None", + onClick: () => setSecondarySortBy("None") + }); + const docFilterOptions: ICustomDropdownItem[] = filterOptions.map((option) => ({ + selected: option === docFilter, + text: option, + onClick: () => handleDocFilterSelection(option) + })); + + const sortedDocumentGroups = sortedDocuments.sortBy( + primarySortBy === sortTagPrompt ? "Strategy" : primarySortBy as PrimarySortType + ); + const secondarySearchTerm = secondarySortBy === sortTagPrompt ? "Strategy" : secondarySortBy as SecondarySortType; const tabState = persistentUI.tabs.get(ENavTab.kSortWork); const openDocumentKey = tabState?.openDocuments.get(ENavTab.kSortWork) || ""; const showSortWorkDocumentArea = !!openDocumentKey; + useEffect(()=>{ + sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); + }, [docFilter, unit.code, investigation.ordinal, problem.ordinal, sortedDocuments]); + return (
{ showSortWorkDocumentArea ? : <> - -
- { renderedSortedDocuments && - renderedSortedDocuments.map((sortedSection, idx) => { + +
+ { sortedDocumentGroups && + sortedDocumentGroups.map((documentGroup: DocumentGroup, idx: number) => { return ( - ); }) diff --git a/src/components/document/sorted-documents.tsx b/src/components/document/sorted-documents.tsx deleted file mode 100644 index 094ae0fa7b..0000000000 --- a/src/components/document/sorted-documents.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from "react"; -import { observer } from "mobx-react"; -import { DocumentContextReact } from "./document-context"; -import classNames from "classnames"; -import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; -import { SortedDocument } from "../../models/stores/sorted-documents"; -import { DocumentModelType, getDocumentContext } from "../../models/document/document"; -import { DecoratedDocumentThumbnailItem } from "../thumbnail/decorated-document-thumbnail-item"; -import { useStores } from "../../hooks/use-stores"; -import { logDocumentViewEvent } from "../../models/document/log-document-event"; -import { ENavTab } from "../../models/view/nav-tabs"; - -import "./sort-work-view.scss"; - -interface IProps { - idx: number; - sortedSection: SortedDocument -} - -export const SortedDocuments: React.FC = observer(function SortedDocuments(props: IProps) { - const { idx, sortedSection } = props; - const { persistentUI } = useStores(); - - const [showDocuments, setShowDocuments] = useState(false); - - const handleSelectDocument = (document: DocumentModelType) => { - persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); - logDocumentViewEvent(document); - }; - - const handleToggleShowDocuments = () => { - setShowDocuments(!showDocuments); - }; - - return ( -
-
-
-
- {sortedSection.icon ? : null} {sortedSection.sectionLabel} -
-
-
Total workspaces: {sortedSection.documents.length}
- -
-
-
-
- {showDocuments && sortedSection.documents.map((doc: any, sortIdx: number) => { - const documentContext = getDocumentContext(doc); - return ( - - - - ); - })} -
-
- ); -}); diff --git a/src/components/document/sorted-section.scss b/src/components/document/sorted-section.scss new file mode 100644 index 0000000000..12abe4ddd2 --- /dev/null +++ b/src/components/document/sorted-section.scss @@ -0,0 +1,103 @@ +@import "../vars"; +$title-margin: 2px; + +.sorted-sections { + width: 100%; + + &.show-documents { + .section-header { + .section-header-label { + border-radius: 5px 5px 0 0; + } + } + .section-sub-header { + height: auto; + margin-bottom: 2px; + min-height: 18px; + padding: 1px 10px; + } + .list { + height: auto; + min-height: 24px; + } + } + + .section-header { + height: 26px; + position: relative; + margin-top: 5px; + margin-bottom: 2px; + + &::after { //divider line drawn across + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 50%; + border-bottom: 1px solid $charcoal-light-1; + } + + .section-header-label { + align-items: center; + background: $classwork-purple-light-7; + border-radius: 5px; + color: $charcoal-dark-2; + display: flex; + height: 26px; + justify-content: space-between; + left: 10px; + padding: 0 5px 0 10px; + position: absolute; + width: calc(100% - 20px); + z-index: 1; + + svg { + margin-right: 5px; + } + + .section-header-left { + display: flex; + align-items: center; + font-weight: bold; + } + + .section-header-right { + display: flex; + align-items: center; + + .section-header-arrow { + cursor: pointer; + fill: $classwork-purple-dark-1; + margin-right: 0; + &.up { + transform: rotate(180deg); + } + } + } + } + } + + .section-sub-header { + background: $classwork-purple-light-5; + font-size: 13px; + font-weight: bold; + height: 0; + margin: 0 10px; + overflow: hidden; + padding: 0; + } + + + .list { + margin: 0 10px; + height: 0; + } + + .loading-spinner { + background-image: url("../../assets/Spinner-1s-200px.svg"); + background-size: contain; + background-repeat: no-repeat; + width: 100px; + height: 75px; + } +} diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx new file mode 100644 index 0000000000..5361b6b936 --- /dev/null +++ b/src/components/document/sorted-section.tsx @@ -0,0 +1,114 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import classNames from "classnames"; + +import { DocumentModelType } from "../../models/document/document"; +import { useStores } from "../../hooks/use-stores"; +import { DocFilterType, SecondarySortType } from "../../models/stores/ui-types"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { DocumentGroup } from "../../models/stores/document-group"; +import { DocumentGroupComponent } from "./document-group"; +import { logDocumentViewEvent } from "../../models/document/log-document-event"; +import { DecoratedDocumentThumbnailItem } from "../thumbnail/decorated-document-thumbnail-item"; +import { ENavTab } from "../../models/view/nav-tabs"; + +import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; + +import "./sorted-section.scss"; + +interface IProps { + docFilter: DocFilterType; + documentGroup: DocumentGroup; + idx: number; + secondarySort: SecondarySortType; +} + +export const SortedSection: React.FC = observer(function SortedDocuments(props: IProps) { + const { docFilter, documentGroup, idx, secondarySort } = props; + const { persistentUI, sortedDocuments } = useStores(); + const [showDocuments, setShowDocuments] = useState(false); + const documentCount = documentGroup.documents?.length || 0; + + const getDocument = (docKey: string) => { + const document = sortedDocuments.documents.all.find((doc: DocumentModelType) => doc.key === docKey); + if (document) return document; + + // Calling `fetchFullDocument` will update the `documents` store with the full document, + // triggering a re-render of this component since it's an observer. + sortedDocuments.fetchFullDocument(docKey); + + return undefined; + }; + + const handleSelectDocument = async (document: DocumentModelType | IDocumentMetadata) => { + persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); + logDocumentViewEvent(document); + }; + + const handleToggleShowDocuments = () => { + setShowDocuments(!showDocuments); + }; + + const renderUngroupedDocument = (doc: IDocumentMetadata) => { + const fullDocument = getDocument(doc.key); + if (!fullDocument) return
; + + return ; + }; + + const renderList = () => { + if (docFilter === "Problem" && secondarySort === "None") { + return documentGroup.documents.map(renderUngroupedDocument); + } + + const renderDocumentGroup = (group: DocumentGroup) => ( + + ); + + return secondarySort === "None" + ? renderDocumentGroup(documentGroup) + : documentGroup.sortBy(secondarySort).map(renderDocumentGroup); + }; + + const sectionClasses = classNames("sorted-sections", {"show-documents": showDocuments}); + + return ( +
+
+
+
+ {documentGroup.icon && } {documentGroup.label} +
+
+
Total workspaces: {documentCount}
+ +
+
+
+ {secondarySort !== "None" && +
+ {secondarySort} +
+ } +
+ {showDocuments && renderList()} +
+
+ ); +}); diff --git a/src/components/navigation/nav-tab-panel.sass b/src/components/navigation/nav-tab-panel.sass index fb9d9c1ccf..63ab7ab051 100644 --- a/src/components/navigation/nav-tab-panel.sass +++ b/src/components/navigation/nav-tab-panel.sass @@ -80,6 +80,12 @@ background-color: $support-blue-light-4 &:active background-color: $support-blue-light-3 + &.sort-work + border-color: $classwork-purple + &:hover + background-color: $classwork-purple-light-4 + &:active + background-color: $classwork-purple-light-3 .new-comment-badge position: absolute top: -2px diff --git a/src/components/navigation/network-documents-section.tsx b/src/components/navigation/network-documents-section.tsx index ba614e80bf..2a5a24ebdf 100644 --- a/src/components/navigation/network-documents-section.tsx +++ b/src/components/navigation/network-documents-section.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { INetworkResourceClassResponse } from "../../../functions/src/shared"; +import { INetworkResourceClassResponse } from "../../../shared/shared"; import { useNetworkResources } from "../../hooks/network-resources"; import { useStores } from "../../hooks/use-stores"; import { DocumentModelType } from "../../models/document/document"; diff --git a/src/components/navigation/sort-work-header-dropdown.scss b/src/components/navigation/sort-work-header-dropdown.scss deleted file mode 100644 index c8105ad256..0000000000 --- a/src/components/navigation/sort-work-header-dropdown.scss +++ /dev/null @@ -1,61 +0,0 @@ -@import "../../components/vars"; - -.sort-work-header { - height: 36px; - width: 100%; - background-color: $classwork-purple-light-4; - display: flex; - - .header-text { - width: 50px; - display: flex; - align-items: center; - margin-left: 10px; - } - - .header-dropdown { - display: flex; - align-items: center; - - .custom-select.sort-work-sort-menu { - color: $charcoal-dark-2; - .header { - min-width: 151px; - width: auto; - height: 26px; - background-color: $classwork-purple-light-4; - padding-left: 5px; - font-weight: bold; - } - .header.show-list { - background-color: $classwork-purple-dark-1; - } - - .list { - min-width: 151px; - width: auto; - margin-top: -1px; - - .list-item { - height: 30px; - padding-left:9px; - - &:hover { - background-color: $classwork-purple-light-5; - } - &.selected { - color:$charcoal-dark-2; - font-weight: bold; - } - &:active { - color:$charcoal-dark-2; - font-weight: bold; - } - div.check { - display: none; - } - } - } - } - } -} diff --git a/src/components/navigation/sort-work-header.scss b/src/components/navigation/sort-work-header.scss new file mode 100644 index 0000000000..4fd468b837 --- /dev/null +++ b/src/components/navigation/sort-work-header.scss @@ -0,0 +1,88 @@ +@import "../../components/vars"; + +.sort-filter-menu-container { + background-color: $classwork-purple-light-5; + border: solid 1.5px $classwork-purple; + border-top: none; + align-items: center; + display: flex; + + .sort-work-header, .filter-work-header { + display: flex; + height: 36px; + + .header-text { + align-items: center; + display: flex; + margin: 0 6px 0 8px; + + + &.secondary { + margin: 0 5px 0 0; + } + } + + .header-dropdown { + display: flex; + align-items: center; + + .custom-select.sort-work-sort-menu, + .custom-select.filter-work-menu { + color: $charcoal-dark-2; + margin-right: 5px; + + .header { + min-width: 151px; + width: auto; + height: 26px; + background-color: $classwork-purple-light-4; + padding-left: 5px; + font-weight: bold; + } + .header.show-list { + background-color: $classwork-purple-dark-1; + } + + .list { + min-width: 151px; + width: auto; + margin-top: -1px; + + .list-item { + height: 30px; + padding-left:9px; + + &:hover { + background-color: $classwork-purple-light-5; + } + &.disabled { + opacity: .35; + pointer-events: none; + } + &.selected { + color:$charcoal-dark-2; + font-weight: bold; + } + &:active { + color:$charcoal-dark-2; + font-weight: bold; + } + div.check { + display: none; + } + } + } + } + } + } + .sort-work-header { + flex-grow: 1; + } + .filter-work-header { + justify-content: flex-end; + + .header-text { + margin-left: 0; + } + } +} diff --git a/src/components/navigation/sort-work-header.tsx b/src/components/navigation/sort-work-header.tsx index a5c3f9dad6..17a599f6ba 100644 --- a/src/components/navigation/sort-work-header.tsx +++ b/src/components/navigation/sort-work-header.tsx @@ -1,26 +1,55 @@ import { observer } from "mobx-react"; import React from "react"; -import { CustomSelect } from "../../clue/components/custom-select"; +import { CustomSelect, ICustomDropdownItem } from "../../clue/components/custom-select"; -import "./sort-work-header-dropdown.scss"; +import "./sort-work-header.scss"; interface ISortHeaderProps{ - sortBy: string; - sortByOptions: any[] + docFilter: string; + docFilterItems: ICustomDropdownItem[]; + primarySort: string; + primarySortItems: ICustomDropdownItem[]; + secondarySort: string; + secondarySortItems: ICustomDropdownItem[]; } -export const SortWorkHeader:React.FC= observer(function SortWorkView({sortBy, sortByOptions}){ +export const SortWorkHeader:React.FC= observer(function SortWorkView(props){ + const { docFilter, docFilterItems, primarySort, primarySortItems, secondarySort, secondarySortItems } = props; return ( -
-
Sort by
-
- +
+
+
Sort by
+
+ +
+
then
+
+ +
+
+
+
Show for
+
+ +
); diff --git a/src/components/qa-clear.tsx b/src/components/qa-clear.tsx deleted file mode 100644 index 4e58e25781..0000000000 --- a/src/components/qa-clear.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { clearFirebaseAnonQAUser } from "../lib/db-clear"; - -export const QAClear: React.FC = () => { - const [qaCleared, setQACleared] = useState(false); - - useEffect(() => { - clearFirebaseAnonQAUser() - .then(() => setQACleared(true)); - }, []); - - return ( - - {qaCleared ? `QA Cleared: OK` : "QA Clearing..."} - - ); -}; - diff --git a/src/components/themes.scss b/src/components/themes.scss index ee581c970f..9de3b69478 100644 --- a/src/components/themes.scss +++ b/src/components/themes.scss @@ -66,6 +66,15 @@ background-color: $learninglog-green-light-2; } } + &.sort-work { + fill: $classwork-purple; + &:hover { + background-color: $classwork-purple-light-4; + } + &:active { + background-color: $classwork-purple-light-3; + } + } &.no-action { cursor: none; &:hover { diff --git a/src/components/thumbnail/collapsible-document-section.tsx b/src/components/thumbnail/collapsible-document-section.tsx index c471c8c3b0..82a6498a33 100644 --- a/src/components/thumbnail/collapsible-document-section.tsx +++ b/src/components/thumbnail/collapsible-document-section.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { DocumentContextReact } from "../document/document-context"; -import { INetworkResourceClassResponse } from "../../../functions/src/shared"; +import { INetworkResourceClassResponse } from "../../../shared/shared"; import { DocumentModelType, getDocumentContext } from "../../models/document/document"; import { IStores } from "../../models/stores/stores"; import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; diff --git a/src/components/thumbnail/decorated-document-thumbnail-item.tsx b/src/components/thumbnail/decorated-document-thumbnail-item.tsx index d0ad1bc58d..e5dbb771cb 100644 --- a/src/components/thumbnail/decorated-document-thumbnail-item.tsx +++ b/src/components/thumbnail/decorated-document-thumbnail-item.tsx @@ -36,7 +36,7 @@ export const DecoratedDocumentThumbnailItem: React.FC = observer(({ const { bookmarks } = useStores(); // sync delete a publication to firebase - useDocumentSyncToFirebase(user, dbStore.firebase, document, true); + useDocumentSyncToFirebase(user, dbStore.firebase, dbStore.firestore, document, true); function handleDocumentDragStart(e: React.DragEvent) { e.dataTransfer.setData(DocumentDragKey, document.key); diff --git a/src/components/thumbnail/document-type-collection.scss b/src/components/thumbnail/document-type-collection.scss index bf85d65690..e905b6aaec 100644 --- a/src/components/thumbnail/document-type-collection.scss +++ b/src/components/thumbnail/document-type-collection.scss @@ -63,8 +63,9 @@ $section-padding: 6px; padding: $padding 0; display: flex; flex-direction: column; + flex-grow: 0; + flex-shrink: 0; align-items: center; - background-color: white; overflow: hidden; cursor: pointer; diff --git a/src/components/thumbnail/documents-type-collection.tsx b/src/components/thumbnail/documents-type-collection.tsx index d11753e96a..7e5c84de68 100644 --- a/src/components/thumbnail/documents-type-collection.tsx +++ b/src/components/thumbnail/documents-type-collection.tsx @@ -49,12 +49,12 @@ export const DocumentCollectionByType: React.FC = observer(({ const appConfigStore = useAppConfig(); const classStore = useClassStore(); const user = useUserStore(); - const { sortedDocuments } = useStores(); + const { sectionDocuments } = useStores(); const showNewDocumentThumbnail = section.addDocument && !!onSelectNewDocument; const newDocumentLabel = getNewDocumentLabel(section, appConfigStore); const isSinglePanel = numSections < 2; const tabName = tab?.toLowerCase().replace(' ', '-'); - const sectionDocs = sortedDocuments.getSectionDocs(section); + const sectionDocs = sectionDocuments.getSectionDocs(section); const isTopPanel = index === 0 && numSections > 1; const isBottomPanel = index > 0 && index === numSections - 1; diff --git a/src/components/thumbnail/simple-document-item.scss b/src/components/thumbnail/simple-document-item.scss new file mode 100644 index 0000000000..4e09572358 --- /dev/null +++ b/src/components/thumbnail/simple-document-item.scss @@ -0,0 +1,34 @@ +@import "../vars"; + +.simple-document-item { + background: $classwork-purple-light-7; + border: solid 1px $charcoal; + border-radius: 1px; + cursor: pointer; + display: flex; + flex-grow: 0; + flex-shrink: 0; + height: 16px; + margin: 0; + width: 16px; + + &:hover { + background: $classwork-purple-light-4; + } + &:active { + background: $classwork-purple-light-2; + } + + &.private { + background: $charcoal-light-7; + border: dotted 1px $charcoal; + cursor: not-allowed; + + &:hover { + background: $charcoal-light-4; + } + &:active { + background: $color3; + } + } +} diff --git a/src/components/thumbnail/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx new file mode 100644 index 0000000000..ba48cbaa6a --- /dev/null +++ b/src/components/thumbnail/simple-document-item.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { useStores } from "../../hooks/use-stores"; +import { isDocumentAccessibleToUser } from "../../models/document/document-utils"; + +import "./simple-document-item.scss"; + +interface IProps { + document: IDocumentMetadata; + investigationOrdinal: string; + problemOrdinal: string; + onSelectDocument: (document: IDocumentMetadata) => void; +} + +export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDocument, problemOrdinal }: IProps) => { + const { documents, class: classStore, unit, user } = useStores(); + const { uid } = document; + const userName = classStore.getUserById(uid)?.displayName; + const investigations = unit.investigations; + // TODO: Make it so we don't have to convert investigationOrdinal and problemOrdinal to numbers here? We do so + // because the values originate as strings. Changing their types to numbers in the model would make this unnecessary, + // but doing that causes errors elsewhere when trying to load documents that aren't associated with a problem. + const investigation = investigations[Number(investigationOrdinal)]; + const problem = investigation?.problems[Number(problemOrdinal) - 1]; + const title = document.title ? `${userName}: ${document.title}` : `${userName}: ${problem?.title ?? "unknown title"}`; + const isPrivate = !isDocumentAccessibleToUser(document, user, documents); + + const handleClick = () => { + onSelectDocument(document); + }; + + return ( +
+
+ ); +}; diff --git a/src/doc-editor.tsx b/src/doc-editor.tsx index b30a8dbc3d..888e4b9ee2 100644 --- a/src/doc-editor.tsx +++ b/src/doc-editor.tsx @@ -4,13 +4,12 @@ import { ChakraProvider, extendTheme } from "@chakra-ui/react"; import { mode } from "@chakra-ui/theme-tools"; import { DocEditorApp } from "./components/doc-editor/doc-editor-app"; import { DialogComponent } from "./components/utilities/dialog"; -import { urlParams } from "./utilities/url-params"; import { AppProvider, initializeApp } from "./initialize-app"; (window as any).DISABLE_FIREBASE_SYNC = true; -const stores = initializeApp(urlParams.appMode || "dev", true); +const stores = initializeApp(true); // By default Chakra adds some global styles which break some of the // CLUE styles. But removing these styles then breaks the Chakra diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index e1d495a8d5..41b4706035 100644 --- a/src/hooks/document-comment-hooks.ts +++ b/src/hooks/document-comment-hooks.ts @@ -3,7 +3,7 @@ import { useCallback } from "react"; import { useMutation, UseMutationOptions, useQuery, useQueryClient } from "react-query"; import { ICurriculumMetadata, IDocumentMetadata, IPostDocumentCommentParams, isCurriculumMetadata, isDocumentMetadata, isSectionPath -} from "../../functions/src/shared"; +} from "../../shared/shared"; import { CommentDocument, CurriculumDocument, DocumentDocument } from "../lib/firestore-schema"; import { useCollectionOrderedRealTimeQuery, useFirestore, WithId } from "./firestore-hooks"; import { useFirebaseFunction } from "./use-firebase-function"; @@ -109,6 +109,7 @@ type PostDocumentCommentUseMutationOptions = export const usePostDocumentComment = (options?: PostDocumentCommentUseMutationOptions) => { const queryClient = useQueryClient(); + const [firestore] = useFirestore(); const postDocumentComment = useFirebaseFunction("postDocumentComment_v1"); const context = useUserContext(); const postComment = useCallback((clientParams: IPostDocumentCommentClientParams) => { @@ -120,6 +121,35 @@ export const usePostDocumentComment = (options?: PostDocumentCommentUseMutationO onMutate: async newCommentParams => { const { document, comment } = newCommentParams; const queryKey = getCommentsQueryKeyFromMetadata(document); + + // update metadata document with the new tags + const tags = comment.tags || []; + const documentKey = isDocumentMetadata(document) ? document.key : undefined; + if (documentKey && tags.length > 0) { + // Just the document key is not enough for Firestore to know that we have permission + // to access the document. Providing a context_id gives the rules enough info to make + // sure we have access to the returned docs. The `context.classHash` used here is + // the current class that CLUE has been run in. However, a teacher might be commenting + // on a document from a different class. In that case the code below will not find + // the documents that need to have their strategies updated. + // Instead we should be using the context_id of the document that we are commenting on. + // CLUE tries to track the context_id when it loads a remote document. That + // information should be stored in the DocumentModel#remoteContext field. We don't + // have access to the DocumentModel here though, just document.metadata. + // FIXME: provide access to remoteContext here so we can update strategies on remote + // documents. Alternatively move this into a firebase function instead of doing this + // in the client. + const metadataQuery = firestore.collection("documents") + .where("key", "==", documentKey) + .where("context_id", "==", context.classHash); + metadataQuery.get().then(querySnapshot => { + querySnapshot.docs.forEach(doc => { + const docRef = doc.ref; + docRef.update({ strategies: firebase.firestore.FieldValue.arrayUnion(...tags) }); + }); + }); + } + // snapshot the current state of the comments in case we need to roll back on error const rollbackComments = queryKey && queryClient.getQueryData(queryKey); type CommentWithId = WithId; diff --git a/src/hooks/firestore-hooks.ts b/src/hooks/firestore-hooks.ts index 3c1a9b6a26..a11c4cc4d1 100644 --- a/src/hooks/firestore-hooks.ts +++ b/src/hooks/firestore-hooks.ts @@ -3,6 +3,7 @@ import { observable } from "mobx"; import { useCallback, useEffect } from 'react'; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from 'react-query'; import { UserDocument } from "../lib/firestore-schema"; +import { typeConverter } from "../utilities/db-utils"; import { useDBStore } from './use-stores'; export type WithId = T & { id: string }; @@ -41,14 +42,6 @@ export function useFirestoreTeacher(uid: string, network: string) { return firestoreTeachers.get(uid); } -// https://medium.com/swlh/using-firestore-with-typescript-65bd2a602945 -const defaultConverter = (): - firebase.firestore.FirestoreDataConverter => -({ - toFirestore: (data: T) => data, - fromFirestore: (doc: firebase.firestore.QueryDocumentSnapshot) => doc.data() as T -}); - export interface IUseOrderedCollectionRealTimeQuery { converter?: firebase.firestore.FirestoreDataConverter; orderBy?: string; @@ -56,7 +49,7 @@ export interface IUseOrderedCollectionRealTimeQuery { } export function useCollectionOrderedRealTimeQuery( partialPath: string, options?: IUseOrderedCollectionRealTimeQuery) { - const { converter = defaultConverter(), orderBy, useQueryOptions: _useQueryOptions } = options || {}; + const { converter = typeConverter(), orderBy, useQueryOptions: _useQueryOptions } = options || {}; const queryClient = useQueryClient(); const [db, root] = useFirestore(); const fsPath = partialPath ? `${root}/${partialPath}` : ""; diff --git a/src/hooks/network-resources.test.ts b/src/hooks/network-resources.test.ts index 2d31dc7dc3..45d918193f 100644 --- a/src/hooks/network-resources.test.ts +++ b/src/hooks/network-resources.test.ts @@ -1,5 +1,5 @@ import { renderHook } from "@testing-library/react-hooks"; -import { INetworkResourceClassResponse } from "../../functions/src/shared"; +import { INetworkResourceClassResponse } from "../../shared/shared"; import { useNetworkResources } from "./network-resources"; const mockGetNetworkResources = jest.fn(() => Promise.resolve({ diff --git a/src/hooks/network-resources.ts b/src/hooks/network-resources.ts index 3f5a084012..6a0c92b6e4 100644 --- a/src/hooks/network-resources.ts +++ b/src/hooks/network-resources.ts @@ -1,7 +1,7 @@ import { each } from "lodash"; import { useCallback } from "react"; import { useQuery } from "react-query"; -import { IGetNetworkResourcesParams, IGetNetworkResourcesResponse } from "../../functions/src/shared"; +import { IGetNetworkResourcesParams, IGetNetworkResourcesResponse } from "../../shared/shared"; import { DBOfferingUserProblemDocument, DBOtherDocument, DBOtherPublication, DBPublication } from "../lib/db-types"; import { createDocumentModel } from "../models/document/document"; import { diff --git a/src/hooks/use-document-sync-to-firebase.test.ts b/src/hooks/use-document-sync-to-firebase.test.ts index 74cf9c488f..acf31f07b0 100644 --- a/src/hooks/use-document-sync-to-firebase.test.ts +++ b/src/hooks/use-document-sync-to-firebase.test.ts @@ -1,8 +1,11 @@ import { renderHook } from "@testing-library/react-hooks"; +import { waitFor } from "@testing-library/react"; import { observable, reaction, runInAction } from "mobx"; import { SnapshotIn } from "mobx-state-tree"; import { UseMutationOptions } from "react-query"; +import firebase from "firebase/app"; import { Firebase } from "../lib/firebase"; +import { Firestore } from "../lib/firestore"; import { DocumentModel, createDocumentModel } from "../models/document/document"; import { LearningLogDocument, PersonalDocument, PlanningDocument, ProblemDocument @@ -52,11 +55,28 @@ const mockHttpsCallable = jest.fn((fn: string) => { return mockPostDocumentComment_v1; } }); -jest.mock("firebase/app", () => ({ - functions: () => ({ - httpsCallable: (fn: string) => mockHttpsCallable(fn) - }) -})); + + +jest.mock("firebase/app", () => { + // Queries can have where clauses chained off of them + const mockQuery = jest.fn(); + mockQuery.mockReturnValue({ + get: jest.fn().mockResolvedValue({ + docs: [{ ref: { update: jest.fn().mockResolvedValue(undefined) } }] + }), + where: mockQuery + }); + + const mockFirestore = jest.fn().mockReturnValue({ + collection: mockQuery + }); + return { + firestore: mockFirestore, + functions: () => ({ + httpsCallable: (fn: string) => mockHttpsCallable(fn) + }), + }; +}); const mockUpdate = jest.fn(); const mockRef = jest.fn(); @@ -78,6 +98,10 @@ const specFirebase = (type: string, key: string) => { } as unknown as Firebase; }; +const specFirestore = () => { + return (firebase.firestore()) as unknown as Firestore; +}; + const specDocument = (overrides?: Partial>) => { const props: SnapshotIn = { type: "problem", key: "doc-key", uid: "1", content: {}, ...overrides }; @@ -89,9 +113,10 @@ const specArgs = (type: string, key: string, documentOverrides?: Partial>) => { const user = specUser(userOverrides); const { id: uid } = user; - const firebase = specFirebase(type, key); + const fb = specFirebase(type, key); + const firestore = specFirestore(); const document = specDocument({ type: type as any, key, uid, ...documentOverrides }); - return { user, firebase, document }; + return { user, fb, firestore, document }; }; describe("useDocumentSyncToFirebase hook", () => { @@ -155,8 +180,8 @@ describe("useDocumentSyncToFirebase hook", () => { }); it("doesn't monitor read-only documents", () => { - const { user, firebase, document } = specArgs(PlanningDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document, true)); + const { user, fb, firestore, document } = specArgs(PlanningDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document, true)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -182,8 +207,8 @@ describe("useDocumentSyncToFirebase hook", () => { }); it("monitors problem documents", async () => { - const { user, firebase, document } = specArgs(ProblemDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(ProblemDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -211,8 +236,8 @@ describe("useDocumentSyncToFirebase hook", () => { }); it("monitors planning documents", () => { - const { user, firebase, document } = specArgs(PlanningDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(PlanningDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -239,8 +264,8 @@ describe("useDocumentSyncToFirebase hook", () => { }); it("monitors personal documents", () => { - const { user, firebase, document } = specArgs(PersonalDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(PersonalDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -269,8 +294,8 @@ describe("useDocumentSyncToFirebase hook", () => { }); it("monitors learning log documents", () => { - const { user, firebase, document } = specArgs(LearningLogDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(LearningLogDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -300,18 +325,15 @@ describe("useDocumentSyncToFirebase hook", () => { it("monitors problem documents with additional logging when DEBUG_SAVE == true", async () => { libDebug.DEBUG_SAVE = true; - const { user, firebase, document } = specArgs(ProblemDocument, "xyz"); + const { user, fb, firestore, document } = specArgs(ProblemDocument, "xyz"); expect.assertions(18); // logs monitoring of document let unmount: () => void; - let waitFor: (callback: () => boolean | void) => Promise; await jestSpyConsole("log", async spy => { - const { unmount: _unmount, waitFor: _waitFor } = - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { unmount: _unmount } = renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); unmount = _unmount; - waitFor = _waitFor; await waitFor(() => expect(spy).toBeCalledTimes(1)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -368,31 +390,27 @@ describe("useDocumentSyncToFirebase hook", () => { it("monitors personal documents with additional logging when DEBUG_SAVE == true", async () => { libDebug.DEBUG_SAVE = true; - const { user, firebase, document } = specArgs(PersonalDocument, "xyz"); + const { user, fb, firestore, document } = specArgs(PersonalDocument, "xyz"); expect.assertions(19); // logs monitoring of document let unmount: () => void; - let waitFor: (callback: () => boolean | void) => Promise; await jestSpyConsole("log", async spy => { - const { unmount: _unmount, waitFor: _waitFor } = - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { unmount: _unmount } = renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); unmount = _unmount; - waitFor = _waitFor; await waitFor(() => expect(spy).toBeCalledTimes(1)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); }); // responds to visibility change - await jestSpyConsole("log", spy => { + await jestSpyConsole("log", async spy => { document.setVisibility("public"); - jest.runAllTimers(); - expect(spy).not.toBeCalled(); + await waitFor(() => expect(mockRef).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(mockUpdate).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(spy).toBeCalledTimes(1)); }); - expect(mockRef).toHaveBeenCalledTimes(1); - expect(mockUpdate).toHaveBeenCalledTimes(1); // saves when title changes with additional logging mockRef.mockClear(); @@ -437,9 +455,9 @@ describe("useDocumentSyncToFirebase hook", () => { it("warns when asked to monitor another user's document", async () => { libDebug.DEBUG_SAVE = false; - const { user, firebase, document } = specArgs(PersonalDocument, "xyz", {}, { uid: "2" }); + const { user, fb, firestore, document } = specArgs(PersonalDocument, "xyz", {}, { uid: "2" }); jestSpyConsole("warn", mockConsole => { - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockConsole).toHaveBeenCalledTimes(1); }); expect(mockRef).toHaveBeenCalledTimes(0); @@ -458,8 +476,8 @@ describe("useDocumentSyncToFirebase hook", () => { .mockImplementationOnce(() => Promise.reject("No save for you!")) .mockImplementationOnce(value => Promise.resolve(value)); - const { user, firebase, document } = specArgs(ProblemDocument, "xyz"); - const { waitFor } = renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(ProblemDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -517,9 +535,8 @@ describe("useDocumentSyncToFirebase hook", () => { .mockImplementationOnce(() => Promise.reject("No save for you!")) .mockImplementationOnce(value => Promise.resolve(value)); - const { user, firebase, document } = specArgs(PersonalDocument, "xyz"); - const { waitFor } = - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(PersonalDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect(mockRef).toHaveBeenCalledTimes(0); expect(mockUpdate).toHaveBeenCalledTimes(0); @@ -577,13 +594,13 @@ describe("useDocumentSyncToFirebase hook", () => { it("sets window.currentDocument when DOCUMENT_DEBUG is true", () => { libDebug.DEBUG_DOCUMENT = true; - const { user, firebase, document } = specArgs(ProblemDocument, "xyz"); - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + const { user, fb, firestore, document } = specArgs(ProblemDocument, "xyz"); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect((window as any).currentDocument).toBe(document); (window as any).currentDocument = undefined; libDebug.DEBUG_DOCUMENT = false; - renderHook(() => useDocumentSyncToFirebase(user, firebase, document)); + renderHook(() => useDocumentSyncToFirebase(user, fb, firestore, document)); expect((window as any).currentDocument).toBeUndefined(); }); }); diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 25aaf28dc7..e9bdac0a27 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -1,4 +1,6 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; +import { throttle as _throttle } from "lodash"; +import { onSnapshot, SnapshotOut } from "mobx-state-tree"; import { useSyncMstNodeToFirebase } from "./use-sync-mst-node-to-firebase"; import { useSyncMstPropToFirebase } from "./use-sync-mst-prop-to-firebase"; import { DEBUG_DOCUMENT, DEBUG_SAVE } from "../lib/debug"; @@ -8,6 +10,11 @@ import { isPublishedType, LearningLogDocument, LearningLogPublication, PersonalD PersonalPublication, ProblemDocument, ProblemPublication, SupportPublication } from "../models/document/document-types"; import { UserModelType } from "../models/stores/user"; +import { Firestore } from "src/lib/firestore"; +import { useMutation, UseMutationOptions } from "react-query"; +import { ITileMapEntry } from "../../shared/shared"; +import { DocumentContentSnapshotType } from "src/models/document/document-content"; +import { IArrowAnnotation } from "src/models/annotations/arrow-annotation"; function debugLog(...args: any[]) { // eslint-disable-next-line no-console @@ -25,7 +32,12 @@ function debugLog(...args: any[]) { * trying to keep track of listeners on all of a user's documents simultaneously. */ export function useDocumentSyncToFirebase( - user: UserModelType, firebase: Firebase, document: DocumentModelType, readOnly = false) { + user: UserModelType, + firebase: Firebase, + firestore: Firestore, + document: DocumentModelType, + readOnly = false +) { const { key, type, uid, contentStatus } = document; const { content: contentPath, metadata, typedMetadata } = firebase.getUserDocumentPaths(user, type, key, uid); @@ -64,6 +76,31 @@ export function useDocumentSyncToFirebase( const commonSyncEnabled = !disableFirebaseSync && contentStatus === ContentStatus.Valid; + /** + * We currently have multiple firestore metadata docs for each real doc. + * Use this function to update a property in all of them. + * + * @param prop + * @param value + * @returns + */ + const updateFirestoreDocumentProp = (prop: string, value?: string | string[]) => { + // The context_id is required so the security rules know we aren't trying + // to get documents we don't have access to. + // We only update document props like visibility, the title, and tools + // when the document is being edited. The document can only be edited + // within its class, so it is safe to use the user.classHash here. + const firestoreMetadataDocs = firestore.collection("documents") + .where("key", "==", document.key) + .where("context_id", "==", user.classHash); + + return firestoreMetadataDocs.get().then((querySnapshot) => { + return Promise.all( + querySnapshot.docs.map((doc) => doc.ref.update({ [prop]: value})) + ); + }); + }; + // sync visibility (public/private) for problem documents useSyncMstPropToFirebase({ firebase, model: document, prop: "visibility", path: typedMetadata, @@ -75,7 +112,8 @@ export function useDocumentSyncToFirebase( onError: (err, visibility) => { console.warn(`ERROR: Failed to update document visibility for ${type} document ${key}:`, visibility); } - } + }, + additionalMutation: updateFirestoreDocumentProp }); // sync visibility (public/private) for personal and learning log documents @@ -89,7 +127,8 @@ export function useDocumentSyncToFirebase( onError: (err, visibility) => { console.warn(`ERROR: Failed to update document visibility for ${type} document ${key}:`, visibility); } - } + }, + additionalMutation: updateFirestoreDocumentProp }); // sync title for personal and learning log documents @@ -103,7 +142,8 @@ export function useDocumentSyncToFirebase( onError: (err, title) => { console.warn(`ERROR: Failed to update document title for ${type} document ${key}:`, title); } - } + }, + additionalMutation: updateFirestoreDocumentProp }); // sync properties for problem, personal, and learning log documents @@ -139,19 +179,73 @@ export function useDocumentSyncToFirebase( }); // sync content for editable document types - useSyncMstNodeToFirebase({ - firebase, model: document.content, path: contentPath, - enabled: commonSyncEnabled && !readOnly && !!document.content && !isPublishedType(type), - transform: snapshot => ({ changeCount: document.incChangeCount(), content: JSON.stringify(snapshot) }), - options: { - onSuccess: (data, snapshot) => { - debugLog(`DEBUG: Updated document content for ${type} document ${key}:`, document.changeCount); - }, - onError: (err, properties) => { - console.warn(`ERROR: Failed to update document content for ${type} document ${key}:`, document.changeCount); - } + const enabled = commonSyncEnabled && !readOnly && !!document.content && !isPublishedType(type); + const options: Omit>, 'mutationFn'> = { + // default is to retry with linear back-off to a maximum + retry: true, + retryDelay: (attempt) => Math.min(attempt * 5, 30), + // but clients may override the defaults + onSuccess: (data: any, snapshot: DocumentContentSnapshotType) => { + debugLog(`DEBUG: Updated document content for ${type} document ${key}:`, document.changeCount); + }, + onError: (err: any, properties: DocumentContentSnapshotType) => { + console.warn(`ERROR: Failed to update document content for ${type} document ${key}:`, document.changeCount); } - }); + }; + const transform = (snapshot: DocumentContentSnapshotType) => + ({ changeCount: document.incChangeCount(), content: JSON.stringify(snapshot) }); + + const mutation = useMutation((snapshot: DocumentContentSnapshotType) => { + const tileMap = snapshot.tileMap || {}; + + const tools: string[] = []; + + Object.keys(tileMap).forEach((tileKey) => { + const tileInfo = tileMap[tileKey] as ITileMapEntry; + const tileType = tileInfo.content.type; + if (!tools.includes(tileType)) { + tools.push(tileType); + } + }); + + // The annotations property does exist on the snapshot but MobX doesn't recognize it + // as a property because of the way we are constructing the DocumentContentModel + // on top of multiple other models. This typing is a workaround so TS doesn't complain. + const annotations = + (snapshot as {annotations: Record}).annotations || {}; + + Object.keys(annotations).forEach((annotationKey: string) => { + const annotation = annotations[annotationKey]; + // for now we only want Sparrow annotations + // we might want to change this if we want to count other types in the future + if (annotation.type === "arrowAnnotation" && !tools.includes("Sparrow")) { + tools.push("Sparrow"); + } + }); + + const promises = []; + + // update tiletypes for metadata document in firestore + promises.push(updateFirestoreDocumentProp("tools", tools)); + + promises.push(firebase.ref(contentPath).update(transform?.(snapshot) ?? snapshot)); + return Promise.all(promises); + }, options); + + const throttledMutate = useMemo(() => _throttle(mutation.mutate, 1000), [mutation.mutate]); + + useEffect(() => { + const cleanup = enabled + ? onSnapshot(document.content!, snapshot => { + // reset (e.g. stop retrying and restart) when value changes + mutation.isError && mutation.reset(); + throttledMutate(snapshot); + }) + : undefined; + return () => { + cleanup?.(); + }; + }, [enabled, document.content, mutation, throttledMutate]); useEffect(() => { DEBUG_SAVE && !readOnly && diff --git a/src/hooks/use-stores.ts b/src/hooks/use-stores.ts index e3f12f30d6..edf15438dc 100644 --- a/src/hooks/use-stores.ts +++ b/src/hooks/use-stores.ts @@ -3,7 +3,7 @@ import { useContext, useMemo } from "react"; import { DB } from "../lib/db"; import { buildSectionPath, getCurriculumMetadata, ICurriculumMetadata, IDocumentMetadata, isSectionPath, networkDocumentKey -} from "../../functions/src/shared"; +} from "../../shared/shared"; import { ProblemModelType } from "../models/curriculum/problem"; import { AppConfigModelType } from "../models/stores/app-config-model"; import { DocumentsModelType } from "../models/stores/documents"; diff --git a/src/hooks/use-sync-mst-prop-to-firebase.ts b/src/hooks/use-sync-mst-prop-to-firebase.ts index f699e30902..d6d1b428d0 100644 --- a/src/hooks/use-sync-mst-prop-to-firebase.ts +++ b/src/hooks/use-sync-mst-prop-to-firebase.ts @@ -21,9 +21,11 @@ interface IProps { shouldMutate?: boolean | ((value: T) => boolean), options?: Omit, 'mutationFn'>; throttle?: number; + additionalMutation?: (prop: string, value: T) => Promise; } export function useSyncMstPropToFirebase({ - firebase, model, prop, path, enabled = true, shouldMutate = true, options: clientOptions, throttle = 1000 + firebase, model, prop, path, enabled = true, shouldMutate = true, options: clientOptions, throttle = 1000, + additionalMutation }: IProps) { const options: Omit, 'mutationFn'> = { @@ -35,7 +37,14 @@ export function useSyncMstPropToFirebase { const should = typeof shouldMutate === "function" ? shouldMutate(value) : shouldMutate; - return should ? firebase.ref(path).update({ [prop]: value }) : Promise.resolve(); + const mutations = should + ? Promise.all([ + firebase.ref(path).update({ [prop]: value }), + additionalMutation ? additionalMutation(prop, value) : Promise.resolve() + ]) + : Promise.resolve(); + + return mutations; }, options); const throttledMutate = useMemo(() => _throttle(mutation.mutate, throttle), [mutation.mutate, throttle]); diff --git a/src/hooks/use-user-context.ts b/src/hooks/use-user-context.ts index 041ef84f79..8a8f1373e4 100644 --- a/src/hooks/use-user-context.ts +++ b/src/hooks/use-user-context.ts @@ -1,4 +1,4 @@ -import { IUserContext } from "../../functions/src/shared"; +import { IUserContext } from "../../shared/shared"; import { useStores } from "./use-stores"; export const useUserContext = (): IUserContext => { diff --git a/src/index.tsx b/src/index.tsx index 26c355f32c..94de827a46 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,10 +3,7 @@ import ReactDOM from "react-dom"; import { AppProvider, initializeApp } from "./initialize-app"; import { AppComponent } from "./components/app"; -import { getAppMode } from "./lib/auth"; -import { urlParams } from "./utilities/url-params"; -import { QAClear } from "./components/qa-clear"; -import { getBearerToken, initializeAuthorization } from "./utilities/auth-utils"; +import { initializeAuthorization } from "./utilities/auth-utils"; import { removeLoadingMessage, showLoadingMessage } from "./utilities/loading-utils"; removeLoadingMessage("Loading the application"); @@ -14,24 +11,14 @@ showLoadingMessage("Initializing"); const redirectingToAuthDomain = initializeAuthorization(); if (!redirectingToAuthDomain) { - const host = window.location.host.split(":")[0]; - const appMode = getAppMode(urlParams.appMode, getBearerToken(urlParams), host); + const stores = initializeApp(false); + stores.ui.setShowDemoCreator(!!stores.showDemoCreator); - if (appMode === "qa" && urlParams.qaClear === "all") { - ReactDOM.render( - , - document.getElementById("app") - ); - } else { - const stores = initializeApp(appMode); - stores.ui.setShowDemoCreator(!!stores.showDemoCreator); - - ReactDOM.render( - - - , - document.getElementById("app") - ); - removeLoadingMessage("Initializing"); - } + ReactDOM.render( + + + , + document.getElementById("app") + ); + removeLoadingMessage("Initializing"); } diff --git a/src/initialize-app.tsx b/src/initialize-app.tsx index bdabf244c2..fc43d37fcb 100644 --- a/src/initialize-app.tsx +++ b/src/initialize-app.tsx @@ -15,6 +15,7 @@ import { IStores } from "./models/stores/stores"; import { UserModel } from "./models/stores/user"; import { urlParams } from "./utilities/url-params"; import { getBearerToken } from "./utilities/auth-utils"; +import { getAppMode } from "./lib/auth"; import { DEBUG_STORES } from "./lib/debug"; import { gImageMap } from "./models/image-map"; import PackageJson from "./../package.json"; @@ -37,30 +38,49 @@ const kEnableLivelinessChecking = false; * @param appMode * @returns */ -export const initializeApp = (appMode: AppMode, authoring?: boolean): IStores => { +export const initializeApp = (authoring: boolean): IStores => { const appVersion = PackageJson.version; + const bearerToken = getBearerToken(urlParams); const user = UserModel.create(); - const showDemoCreator = urlParams.demo; - if (showDemoCreator) { - // Override the app mode when the demo creator is being used. - // `authenticate` is still called when the demo creator is shown - // and with an undefined appMode then it will default to `authed` on - // a remote host. This will cause an error as it looks for a token. - // This error was always happening but for some reason before the app - // was still rendering, and now it doesn't. - appMode = "demo"; + + let appMode: AppMode; + let showDemoCreator = false; + if (authoring) { + // Support appMode=qa even when authoring so we can test some features that only show + // up in the qa appMode + appMode = urlParams.appMode === "qa" ? "qa" : "dev"; + } else { + const host = window.location.host.split(":")[0]; + appMode = getAppMode(urlParams.appMode, bearerToken, host); + + showDemoCreator = !!urlParams.demo; + if (showDemoCreator) { + // Override the app mode when the demo creator is being used. + // `authenticate` is still called when the demo creator is shown + // and with an undefined appMode then it will default to `authed` on + // a remote host. This will cause an error as it looks for a token. + // This error was always happening but for some reason before the app + // was still rendering, and now it doesn't. + appMode = "demo"; + } } const demoName = urlParams.demoName; - const isPreviewing = !!(urlParams.domain && urlParams.domain_uid && !getBearerToken(urlParams)); + const isPreviewing = !!(urlParams.domain && urlParams.domain_uid && !bearerToken); const appConfig = AppConfigModel.create(appConfigSnapshot); const stores = createStores( { appMode, appVersion, appConfig, user, showDemoCreator, demoName, isPreviewing }); - if (DEBUG_STORES) { - (window as any).stores = stores; + // Expose the stores if the debug flag is set or we are running in Cypress + const aWindow = window as any; + + // The Cypress docs say you can just check window.Cypress but after a page reload in + // some cases you have to use window.parent.Cypress + const inCypress = aWindow.Cypress || aWindow.parent?.Cypress; + if (DEBUG_STORES || inCypress) { + aWindow.stores = stores; } diff --git a/src/lib/db-clear.ts b/src/lib/db-clear.ts deleted file mode 100644 index 5e0e6f9f84..0000000000 --- a/src/lib/db-clear.ts +++ /dev/null @@ -1,41 +0,0 @@ -import firebase from "firebase/app"; -import { firebaseConfig } from "./firebase-config"; - -// The problem with this approach is that it can't be used to clear a logged in -// user's data. So we should check if QA is ever used to store a logged in user's -// data. -export const clearFirebaseAnonQAUser = async () => { - - // check for already being initialized for tests - if (firebase.apps.length === 0) { - firebase.initializeApp(firebaseConfig()); - } - - let firebaseUser: firebase.User | undefined; - - // We are ignoring the unsubscribe method returned because this function is only - // used in a one-off way. - firebase.auth().onAuthStateChanged((_firebaseUser) => { - if (_firebaseUser) { - firebaseUser = _firebaseUser; - } - }); - - await firebase.auth().signInAnonymously(); - - if (!firebaseUser) { - throw new Error("Firebase User not set after sign in"); - } - - // Notes: - // 1. This path is defined in firebase.ts, there isn't an easy way to use the - // Firebase class without causing additional Firestore connections So it is - // duplicated here. - // 2. Firebase looks up the user from the browser's indexedDb. In cypress this - // is not cleared between test runs. So the same anonymous user will be - // used each time. - const qaUser = firebase.database().ref(`/qa/${firebaseUser.uid}`); - if (qaUser) { - await qaUser.remove(); - } -}; diff --git a/src/lib/db-listeners/index.test.ts b/src/lib/db-listeners/index.test.ts index 44119e3fd1..557c06ada6 100644 --- a/src/lib/db-listeners/index.test.ts +++ b/src/lib/db-listeners/index.test.ts @@ -18,7 +18,9 @@ describe("DBListeners", () => { const db = new DB(); beforeEach(async () => { - await db.connect({appMode: "test", stores, dontStartListeners: true}); + // NOTE: for better or worse this is actually connecting to Firebase for real + // in other tests the Firebase library is mocked. + await db.connect({appMode: "test", stores, dontStartListeners: true, authPersistence: "none"}); }); afterEach(() => { diff --git a/src/lib/db-types.ts b/src/lib/db-types.ts index c8257217fe..715eb4b288 100644 --- a/src/lib/db-types.ts +++ b/src/lib/db-types.ts @@ -52,6 +52,7 @@ export interface DBBaseDocumentMetadata { documentKey: string; }; createdAt: number; + title?: string; type: DBDocumentType; // previously in DBOtherDocument properties?: IDocumentProperties; @@ -60,7 +61,10 @@ export interface DBBaseDocumentMetadata { export interface DBBaseProblemDocumentMetadata extends DBBaseDocumentMetadata { classHash: string; + investigation?: string; offeringId: string; + problem?: string; + unit?: string; } export interface DBSectionDocumentMetadataDEPRECATED extends DBBaseProblemDocumentMetadata { diff --git a/src/lib/db.test.ts b/src/lib/db.test.ts index 554e4a08c7..bfb3e9ccb0 100644 --- a/src/lib/db.test.ts +++ b/src/lib/db.test.ts @@ -22,7 +22,7 @@ const mockFunctions = jest.fn(); const mockAuthStateUnsubscribe = jest.fn(); jest.mock("firebase/app", () => { - return { + const mockFirebase = { apps: [], initializeApp: () => null, auth: () => ({ @@ -30,12 +30,15 @@ jest.mock("firebase/app", () => { callback({ uid: "user-id" }); return mockAuthStateUnsubscribe; }, - signInAnonymously: () => Promise.resolve() + signInAnonymously: () => Promise.resolve(), + setPersistence: (persistence: string) => Promise.resolve() }), database: () => mockDatabase(), firestore: () => mockFirestore(), functions: () => mockFunctions() }; + (mockFirebase.auth as any).Auth = { Persistence: { SESSION: "session"}}; + return mockFirebase; }); type QueryParams = UrlParams.QueryParams; diff --git a/src/lib/db.ts b/src/lib/db.ts index 590511074f..826f9f5960 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -30,7 +30,8 @@ import { Firestore } from "./firestore"; import { DBListeners } from "./db-listeners"; import { Logger } from "./logger"; import { LogEventName } from "./logger-types"; -import { IGetImageDataParams, IPublishSupportParams } from "../../functions/src/shared"; +import { getDocumentPath, ICommentableDocumentParams, IDocumentMetadata, IGetImageDataParams, + IPublishSupportParams } from "../../shared/shared"; import { getFirebaseFunction } from "../hooks/use-firebase-function"; import { IStores } from "../models/stores/stores"; import { TeacherSupportModelType, SectionTarget, AudienceModelType } from "../models/stores/supports"; @@ -39,11 +40,15 @@ import { urlParams } from "../utilities/url-params"; import { firebaseConfig } from "./firebase-config"; import { UserModelType } from "../models/stores/user"; import { logExemplarDocumentEvent } from "../models/document/log-exemplar-document-event"; +import { DEBUG_FIRESTORE } from "./debug"; export type IDBConnectOptions = IDBAuthConnectOptions | IDBNonAuthConnectOptions; export interface IDBBaseConnectOptions { stores: IStores; - dontStartListeners?: boolean; // for unit tests + + // for unit tests + dontStartListeners?: boolean; + authPersistence?: firebase.auth.Auth.Persistence; } export interface IDBAuthConnectOptions extends IDBBaseConnectOptions { appMode: "authed"; @@ -62,8 +67,6 @@ export interface GroupUsersMap { [key: string]: string[]; } -export type DBClearLevel = "all" | "class" | "offering"; - export interface ICreateOtherDocumentParams { title?: string; properties?: IDocumentProperties; @@ -87,6 +90,9 @@ export interface OpenDocumentOptions { groupUserConnections?: Record; originDoc?: string; pubVersion?: number; + problem?: string; + investigation?: string; + unit?: string; } export class DB { @@ -97,6 +103,7 @@ export class DB { public stores: IStores; private authStateUnsubscribe?: firebase.Unsubscribe; + private documentFetchPromiseMap = new Map>(); constructor() { makeObservable(this); @@ -110,6 +117,9 @@ export class DB { } public connect(options: IDBConnectOptions) { + if (DEBUG_FIRESTORE) { + firebase.firestore.setLogLevel('debug'); + } return new Promise((resolve, reject) => { if (this.firebase.isConnected) { reject("Already connected to database!"); @@ -157,6 +167,10 @@ export class DB { this.firestore.setFirebaseUser(firebaseUser); if (!options.dontStartListeners) { const { persistentUI, user, db, unitLoadedPromise, exemplarController} = this.stores; + + // Record launch time in Firestore + this.firestore.recordLaunchTime(); + // Start fetching the persistent UI. We want this to happen as early as possible. persistentUI.initializePersistentUISync(user, db); @@ -172,6 +186,10 @@ export class DB { } }); + // SESSION auth persistence is used so each new tab or window gets its own Firebase authentication + // Unless overridden this applies to all app modes (qa, dev, app, auth, test) + firebase.auth().setPersistence(options.authPersistence || firebase.auth.Auth.Persistence.SESSION); + if (options.appMode === "authed") { firebase.auth() .signOut() @@ -392,9 +410,51 @@ export class DB { }); } - public createDocument(params: { type: DBDocumentType, content?: string }) { - const { type, content } = params; - const {user} = this.stores; + async createFirestoreMetadataDocument(metadata: DBDocumentMetadata, documentKey: string) { + const userContext = this.stores.userContextProvider.userContext; + + if (!this.stores.userContextProvider || !this.firestore || !userContext?.uid) { + console.error("cannot create Firestore metadata document because environment is not valid", + { userContext, firestore: this.firestore }); + throw new Error("cannot create Firestore metadata document because environment is not valid"); + } + + const documentPath = getDocumentPath(userContext.uid, documentKey, userContext.network); + const documentRef = this.firestore.doc(documentPath); + const docSnapshot = await documentRef.get(); + + if (!docSnapshot.exists) { + const { classHash, self, version, ...cleanedMetadata } = metadata as DBDocumentMetadata & { classHash: string }; + const firestoreMetadata: IDocumentMetadata & { contextId: string } = { + ...cleanedMetadata, + // The validateCommentableDocument firebase function currently deployed to production is out of date. + // It requires contextId to defined, but doesn't check its value. + contextId: "ignored", + key: documentKey, + properties: {}, + uid: userContext.uid, + unit: null + }; + if ("offeringId" in metadata && metadata.offeringId != null) { + const { investigation, problem, unit } = this.stores; + const investigationOrdinal = String(investigation.ordinal); + const problemOrdinal = String(problem.ordinal); + const unitCode = unit.code; + firestoreMetadata.investigation = investigationOrdinal; + firestoreMetadata.problem = problemOrdinal; + firestoreMetadata.unit = unitCode; + } + const validateCommentableDocument = + getFirebaseFunction("validateCommentableDocument_v1"); + // FIXME-HISTORY: rename this function to validateFirestoreDocumentMetadata_v1 + validateCommentableDocument({context: userContext, document: firestoreMetadata}); + } + } + + public async createDocument(params: { type: DBDocumentType, content?: string, title?: string }) { + const { type, content, title } = params; + const { user } = this.stores; + return new Promise<{document: DBDocument, metadata: DBDocumentMetadata}>((resolve, reject) => { const documentRef = this.firebase.ref(this.firebase.getUserDocumentPath(user)).push(); const documentKey = documentRef.key!; @@ -415,7 +475,7 @@ export class DB { case LearningLogDocument: case PersonalPublication: case LearningLogPublication: - metadata = {version, self, createdAt, type}; + metadata = {version, self, createdAt, type, title}; break; case PlanningDocument: case ProblemDocument: @@ -426,7 +486,13 @@ export class DB { } return documentRef.set(document) - .then(() => metadataRef.set(metadata)) + .then(() => { + metadataRef.set(metadata); + return metadataRef.once("value"); + }) + .then((metadataValue) => { + this.createFirestoreMetadataDocument(metadataValue.val(), documentKey); + }) .then(() => { resolve({document, metadata}); }) @@ -490,7 +556,8 @@ export class DB { let pubCount = documentModel.getNumericProperty("pubCount"); documentModel.setNumericProperty("pubCount", ++pubCount); return new Promise<{document: DBDocument, metadata: DBPublicationDocumentMetadata}>((resolve, reject) => { - this.createDocument({ type: publicationType, content }).then(({document, metadata}) => { + this.createDocument({ type: publicationType, content, title: documentModel.title }) + .then(({document, metadata}) => { const publicationPath = publicationType === "personalPublication" ? this.firebase.getPersonalPublicationsPath(user) : this.firebase.getLearningLogPublicationsPath(user); @@ -544,8 +611,12 @@ export class DB { public openDocument(options: OpenDocumentOptions) { const { documents } = this.stores; - const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion} = options; - return new Promise((resolve, reject) => { + const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion, + problem, investigation, unit} = options; + const existingPromise = this.documentFetchPromiseMap.get(documentKey); + if (existingPromise) return existingPromise; + + const documentFetchPromise = new Promise((resolve, reject) => { const {user} = this.stores; const documentPath = this.firebase.getUserDocumentPath(user, documentKey, userId); const metadataPath = this.firebase.getUserDocumentMetadataPath(user, documentKey, userId); @@ -587,7 +658,10 @@ export class DB { createdAt: metadata.createdAt, content: content ? content : {}, changeCount: document.changeCount, - pubVersion + pubVersion, + problem, + investigation, + unit }); } catch (e) { const msg = "Could not open " + @@ -617,6 +691,9 @@ export class DB { reject(msg); }); }); + + this.documentFetchPromiseMap.set(documentKey, documentFetchPromise); + return documentFetchPromise; } public createLearningLogDocument(title?: string) { @@ -633,7 +710,7 @@ export class DB { const docTitle = title || documents.getNextOtherDocumentTitle(user, documentType, baseTitle); return new Promise((resolve, reject) => { - return this.createDocument({ type: documentType, content: JSON.stringify(content) }) + return this.createDocument({ type: documentType, content: JSON.stringify(content), title: docTitle }) .then(({document, metadata}) => { const {documentKey} = document.self; const newDocument: DBOtherDocument = { @@ -712,35 +789,6 @@ export class DB { this.stores.documents.resolveRequiredDocumentPromiseWithNull(document.type); } - public clear(level: DBClearLevel) { - return new Promise((resolve, reject) => { - const {user} = this.stores; - const clearPath = (path?: string) => { - this.firebase.ref(path).remove().then(resolve).catch(reject); - }; - - if (this.stores.appMode !== "qa") { - return reject("db#clear is only available in qa mode"); - } - - if (level === "all") { - return reject("clearing 'all' is handled by clearFirebaseAnonQAUser"); - } - - switch (level) { - case "class": - clearPath(this.firebase.getClassPath(user)); - break; - case "offering": - clearPath(this.firebase.getOfferingPath(user)); - break; - default: - reject(`Invalid clear level: ${level}`); - break; - } - }); - } - public createDocumentModelFromProblemMetadata( type: ProblemOrPlanningDocumentType, userId: string, metadata: DBOfferingUserProblemDocument) { diff --git a/src/lib/debug.ts b/src/lib/debug.ts index bc89d59e8d..f235a5bc51 100644 --- a/src/lib/debug.ts +++ b/src/lib/debug.ts @@ -34,6 +34,7 @@ export const DEBUG_DATAFLOW = debugContains("dataflow"); export const DEBUG_DOC_LIST = debugContains("docList"); export const DEBUG_DOCUMENT = debugContains("document"); export const DEBUG_DROP = debugContains("drop"); +export const DEBUG_FIRESTORE = debugContains("firestore"); export const DEBUG_HISTORY = debugContains("history"); export const DEBUG_IMAGES = debugContains("images"); export const DEBUG_LISTENERS = debugContains("listeners"); diff --git a/src/lib/firestore-schema.ts b/src/lib/firestore-schema.ts index 8359984435..5bc64f31ae 100644 --- a/src/lib/firestore-schema.ts +++ b/src/lib/firestore-schema.ts @@ -112,7 +112,7 @@ export interface OfferingDocument { unit: string; // e.g. "msa" problem: string; // e.g. "1.4" problemPath: string; // e.g. "msa/1/4" - network: string; // network within which this offering instance is available + network?: string; // network within which this offering instance is available } // collection key is `${network}_${offering id}` type OfferingsCollection = FSCollection; @@ -138,7 +138,8 @@ export interface ClassDocument { context_id: string; // portal class hash teacher: string; // name of primary(?) teacher teachers: string[]; // uids of teachers of class - network: string; // network of teacher creating class + network?: string; // network of teacher creating class + networks?: string[]; // networks of all teachers in the class } // collection key is `${network}_${context_id (class hash)}` type ClassesCollection = FSCollection; diff --git a/src/lib/firestore.ts b/src/lib/firestore.ts index eea8ba862e..cb867bbd8c 100644 --- a/src/lib/firestore.ts +++ b/src/lib/firestore.ts @@ -108,6 +108,10 @@ export class Firestore { return docRef.get(); } + public runTransaction(fn: (t: firebase.firestore.Transaction) => Promise) { + return firebase.firestore().runTransaction(fn); + } + /* * Guarantees the existence of the specified document by reading it first and then * creating it if it doesn't already exist. Optionally, client can specify a @@ -156,4 +160,29 @@ export class Firestore { const userDoc = await this.doc(`users/${uid}`).get(); return userDoc.data() as UserDocument | undefined; } + + /** + * Record the lastLaunchTime in the Firestore root. + * + * This is only recorded for dev, qa, test, and demo appModes. + * In the dev, qa, and test modes each user has their own root or a new root + * is created on each test. + * In the demo mode there could be lots of users launching in the same root + * but this number should be manageable, and it will be useful to keep track + * of how old various demo roots are. + * In the auth (portal launch) case lots of users will be launching the same + * root so the lastLaunchTime would be updated too frequently. Also we can use + * logs and portal information to find the last portal launch. + * + * @returns a promise that resolves when the lastLaunchTime has been updated + */ + public async recordLaunchTime() { + const { appMode } = this.db.stores; + + if (!["dev", "qa", "test", "demo"].includes(appMode)) { + return; + } + + return this.doc("").set({lastLaunchTime: this.timestamp()}, {merge: true}); + } } diff --git a/src/lib/teacher-network.test.ts b/src/lib/teacher-network.test.ts index b22299ba77..b988ea6895 100644 --- a/src/lib/teacher-network.test.ts +++ b/src/lib/teacher-network.test.ts @@ -3,12 +3,13 @@ import "firebase/firestore"; import { Firestore } from "./firestore"; import { ClassDocument, OfferingDocument } from "./firestore-schema"; import { IPortalClassInfo, IPortalClassUser } from "./portal-types"; -import { UserModel, UserPortalOffering } from "../models/stores/user"; +import { UserModel, UserModelType, UserPortalOffering } from "../models/stores/user"; import { ClassWithoutTeachers, clearTeachersPromises, getNetworkClassesThatAssignedProblem, getProblemPath, OfferingWithoutTeachers, syncClass, syncOffering, syncTeacherClassesAndOfferings } from "./teacher-network"; import { DB } from "./db"; +import { ClassModel, ClassModelType } from "../models/stores/class"; const mockStores = { appMode: "authed", @@ -34,7 +35,8 @@ const mockCollection = jest.fn((path: string) => { jest.mock("firebase/app", () => ({ firestore: () => ({ collection: mockCollection, - doc: mockDoc + doc: mockDoc, + runTransaction: jest.fn(callback => callback()) }) })); @@ -60,10 +62,16 @@ class MockFirestoreOtherError extends Error { const kPortalJWT = "JWT"; const kTeacher1IdNumeric = 11; const kTeacher1Id = `${kTeacher1IdNumeric}`; -const kTeacher1Name = "Teacher 1"; +const kTeacher1FirstName = "Teacher"; +const kTeacher1LastName = "1"; +const kTeacher1Initials = "T1"; +const kTeacher1Name = kTeacher1FirstName + " " + kTeacher1LastName; const kTeacher1User: IPortalClassUser = { id: "https://concord.org/users/11", user_id: kTeacher1IdNumeric, first_name: "Teacher", last_name: "1" }; +const kTeacherUserModel = UserModel.create({ + type: "teacher", name: kTeacher1Name, id: kTeacher1Id +}); const kClass1IdNumeric = 1; const kClass1Id = `${kClass1IdNumeric}`; @@ -79,6 +87,15 @@ const portalClass1: IPortalClassInfo = { students: [], offerings: [] }; +const portalClass1Model: ClassModelType = ClassModel.create({ + name: kClass1Name, + classHash: kClass1Hash, + users: { + t1: { type: "teacher", id: kTeacher1Id, firstName: kTeacher1FirstName, lastName: kTeacher1LastName, + fullName: kTeacher1Name, initials: kTeacher1Initials } + } +}); + const partClass1: ClassWithoutTeachers = { id: kClass1Id, name: kClass1Name, @@ -161,14 +178,21 @@ describe("Teacher network functions", () => { resetMocks(); }); - const classDocPath = `/authed/test-portal/classes/test-network_${kClass1Hash}`; + const oldClassDocPath = `/authed/test-portal/classes/test-network_${kClass1Hash}`; + const newClassDocPath = `/authed/test-portal/classes/${kClass1Hash}`; it("should do nothing if the class already exists", async () => { - mockDocGet.mockImplementation(() => Promise.resolve(fsClass1)); + mockDocGet.mockImplementation(() => Promise.resolve({ + exists: true, + data: () => fsClass1})); + fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); - expect(mockDocGet).toHaveBeenCalled(); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(mockDoc).toHaveBeenCalledTimes(2); + expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); + expect(mockDoc).toHaveBeenCalledWith(newClassDocPath); + expect(mockDocGet).toHaveBeenCalledTimes(2); expect(mockDocSet).not.toHaveBeenCalled(); return result; }); @@ -178,9 +202,9 @@ describe("Teacher network functions", () => { // !ok response from fetch fetchMock.mockResponseOnce('{}', { status: 500, headers: { 'content-type': 'application/json' } }); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); - expect(mockDocGet).toHaveBeenCalled(); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); + expect(mockDoc).not.toHaveBeenCalled(); + expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; }); @@ -189,9 +213,9 @@ describe("Teacher network functions", () => { mockDocGet.mockImplementation(() => { throw new MockFirestorePermissionsError(); }); fetchMock.mockRejectOnce(new Error()); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); - expect(mockDocGet).toHaveBeenCalled(); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); + expect(mockDoc).not.toHaveBeenCalled(); + expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; }); @@ -200,8 +224,8 @@ describe("Teacher network functions", () => { mockDocGet.mockImplementation(() => { throw new MockFirestoreOtherError(); }); fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); + expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; @@ -211,8 +235,8 @@ describe("Teacher network functions", () => { mockDocGet.mockImplementation(() => { throw new MockFirestorePermissionsError(); }); fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); + expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).toHaveBeenCalledWith(fsClass1); return result; @@ -291,41 +315,76 @@ describe("Teacher network functions", () => { const completeTeacher = UserModel.create({ id: kTeacher1Id, type: "teacher", network: "test-network", portalClassOfferings: [userOffering1(), userOffering2()] }); - it("should do nothing if there is no portal JWT", async () => { + it("should do nothing if the user is not a teacher", () => { + // If this tried to do something it would fail due to the bogus arguments + syncTeacherClassesAndOfferings( + undefined as unknown as Firestore, + {isTeacher: false, network: null} as unknown as UserModelType, + undefined as unknown as ClassModelType + ); + }); + + it("should sync demo class if there is no portal JWT", async () => { const user = UserModel.create({ id: kTeacher1Id, type: "teacher", network: "test-network" }); const firestore = new Firestore(mockDB); - await syncTeacherClassesAndOfferings(firestore, user, ""); - expect(mockDoc).not.toHaveBeenCalled(); - expect(mockDocGet).not.toHaveBeenCalled(); - expect(mockDocSet).not.toHaveBeenCalled(); + await syncTeacherClassesAndOfferings(firestore, user, portalClass1Model, ""); + expect(mockDoc).toHaveBeenCalledTimes(2); + expect(mockDoc).toHaveBeenCalledWith(`/authed/test-portal/classes/test-network_${kClass1Hash}`); + expect(mockDoc).toHaveBeenCalledWith(`/authed/test-portal/classes/${kClass1Hash}`); + expect(mockDocGet).toHaveBeenCalledTimes(2); + expect(mockDocSet).toHaveBeenCalledTimes(2); + expect(mockDocSet).toHaveBeenCalledWith({ + id: "class-hash-1", + context_id: "class-hash-1", + name: "Class 1", + network: "test-network", + networks: ["test-network"], + teacher: "11", + teachers: [ "11" ], + uri: "" + }); }); - it("should do nothing if the user has no offerings", async () => { + it("should sync demo class if the user has no offerings", async () => { const user = UserModel.create({ id: kTeacher1Id, type: "teacher", network: "test-network" }); const firestore = new Firestore(mockDB); - await syncTeacherClassesAndOfferings(firestore, user, kPortalJWT); - expect(mockDoc).not.toHaveBeenCalled(); - expect(mockDocGet).not.toHaveBeenCalled(); - expect(mockDocSet).not.toHaveBeenCalled(); + await syncTeacherClassesAndOfferings(firestore, user, portalClass1Model, kPortalJWT); + expect(mockDoc).toHaveBeenCalledTimes(2); + expect(mockDoc).toHaveBeenCalledWith(`/authed/test-portal/classes/test-network_${kClass1Hash}`); + expect(mockDoc).toHaveBeenCalledWith(`/authed/test-portal/classes/${kClass1Hash}`); + expect(mockDocGet).toHaveBeenCalledTimes(2); + expect(mockDocSet).toHaveBeenCalledTimes(2); + expect(mockDocSet).toHaveBeenCalledWith({ + id: "class-hash-1", + context_id: "class-hash-1", + name: "Class 1", + network: "test-network", + networks: ["test-network"], + teacher: "11", + teachers: [ "11" ], + uri: "" + }); }); - it("should do nothing if the user has no network", async () => { + it("should sync class even if the user has no network", async () => { const user = UserModel.create({ id: kTeacher1Id, type: "teacher", portalClassOfferings: [userOffering1()] }); + fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - await syncTeacherClassesAndOfferings(firestore, user, kPortalJWT); - expect(mockDoc).not.toHaveBeenCalled(); - expect(mockDocGet).not.toHaveBeenCalled(); - expect(mockDocSet).not.toHaveBeenCalled(); + await syncTeacherClassesAndOfferings(firestore, user, portalClass1Model, kPortalJWT); + expect(mockDoc).toHaveBeenCalledTimes(1); + expect(mockDocGet).toHaveBeenCalledTimes(1); + expect(mockDocSet).toHaveBeenCalledTimes(1); + expect(mockDocSet).toHaveBeenCalledWith({...fsClass1, network: undefined}); }); it("should sync classes and offerings when appropriate", async () => { mockDocGet.mockImplementation(() => { throw new MockFirestorePermissionsError(); }); fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - await Promise.all(syncTeacherClassesAndOfferings(firestore, completeTeacher, kPortalJWT)); - expect(mockDoc).toHaveBeenCalledTimes(3); - expect(mockDocGet).toHaveBeenCalledTimes(3); - expect(mockDocSet).toHaveBeenCalledTimes(3); + await syncTeacherClassesAndOfferings(firestore, completeTeacher, portalClass1Model, kPortalJWT); + expect(mockDoc).toHaveBeenCalledTimes(4); + expect(mockDocGet).toHaveBeenCalledTimes(4); + expect(mockDocSet).toHaveBeenCalledTimes(4); }); }); diff --git a/src/lib/teacher-network.ts b/src/lib/teacher-network.ts index 0793464aab..e958a47f00 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -1,6 +1,9 @@ +import firebase from "firebase/app"; import { Optional } from "utility-types"; +import { ClassModelType } from "../models/stores/class"; import { UserModelType } from "../models/stores/user"; -import { Firestore } from "./firestore"; +import { arraysEqualIgnoringOrder } from "../utilities/js-utils"; +import { Firestore, isFirestorePermissionsError } from "./firestore"; import { ClassDocument, OfferingDocument } from "./firestore-schema"; import { IPortalClassInfo } from "./portal-types"; @@ -47,12 +50,14 @@ export function getProblemPath(unit: string, problem: string) { } // synchronize the current teacher's classes and offerings to firestore -export function syncTeacherClassesAndOfferings(firestore: Firestore, user: UserModelType, rawPortalJWT: string) { +export function syncTeacherClassesAndOfferings( + firestore: Firestore, user: UserModelType, classModel: ClassModelType, rawPortalJWT?: string) { const { network } = user; - if (!network) return []; - const promises: Promise[] = []; + // Non teachers are not allowed to update classes and offerings + if (!user.isTeacher) return; + // map portal offerings to classes const userClasses: Record = {}; user.portalClassOfferings.forEach(offering => { @@ -62,36 +67,108 @@ export function syncTeacherClassesAndOfferings(firestore: Firestore, user: UserM } }); + // If the current class has not been set up (eg demo/qa site), add it with some stubbed-in fields. + if (!userClasses[classModel.classHash]) { + userClasses[classModel.classHash] = { + id: classModel.classHash, + context_id: classModel.classHash, + name: classModel.name, + teacher: user.id, + uri: "", + network + }; + } + // synchronize the classes Object.keys(userClasses).forEach(async context_id => { - promises.push(syncClass(firestore, rawPortalJWT, userClasses[context_id])); - }); - - // synchronize the offerings - user.portalClassOfferings.forEach(async offering => { - const { - offeringId: id, activityTitle: name, activityUrl: uri, classHash: context_id, classUrl, - unitCode: unit, problemOrdinal: problem - } = offering; - const problemPath = getProblemPath(unit, problem); - const fsOffering: OfferingWithoutTeachers = { id, name, uri, context_id, unit, problem, problemPath, network }; - promises.push(syncOffering(firestore, rawPortalJWT, classUrl, fsOffering)); + promises.push(syncClass(firestore, rawPortalJWT, userClasses[context_id], user, network)); }); - return promises; + if (network && rawPortalJWT) { + // synchronize the offerings + user.portalClassOfferings.forEach(async offering => { + const { + offeringId: id, activityTitle: name, activityUrl: uri, classHash: context_id, classUrl, + unitCode: unit, problemOrdinal: problem + } = offering; + const problemPath = getProblemPath(unit, problem); + const fsOffering: OfferingWithoutTeachers = { id, name, uri, context_id, unit, problem, problemPath, network }; + promises.push(syncOffering(firestore, rawPortalJWT, classUrl, fsOffering)); + }); + } + return Promise.all(promises); } -export async function syncClass(firestore: Firestore, rawPortalJWT: string, aClass: ClassWithoutTeachers) { - const { uri, context_id, network } = aClass; - if (uri && context_id && network && rawPortalJWT) { - return firestore.guaranteeDocument(`classes/${network}_${context_id}`, async () => { - const teachers = await getClassTeachers(uri, rawPortalJWT); - if (teachers) { - aClass.teachers = teachers; - return aClass; +async function createOrUpdateClassDoc( + firestore: Firestore, docPath: string, aClass: ClassDocument, addNetwork?: string) { + const docRef = firestore.doc(docPath); + return firestore.runTransaction(async (transaction) => { + // Security rules can depend on the contents of the document, so we could get a permissions + // error when trying to read, but still be able to write a document into this location. + let current; + try { + current = await docRef.get(); + } catch (e) { + // Ignore permissions error, but quit on any other problem + if (!isFirestorePermissionsError(e)) { + console.warn("Error retrieving class document:", e); + return; } - }); + } + if (current && current.exists) { + // Update existing doc + const data = current.data() as ClassDocument; + if (!arraysEqualIgnoringOrder(aClass.teachers, data.teachers)) { + await docRef.update({ teachers: aClass.teachers }); + } + // To support the legacy class docs we add the singular network when the classDoc is + // first created. However when updating the document there isn't a need to update + // this legacy singular network. + if (addNetwork && !data.networks?.includes(addNetwork)) { + await docRef.update({ networks: firebase.firestore.FieldValue.arrayUnion(addNetwork) }); + } + } else { + // Create the document. + if (addNetwork) { + // TODO: there could be co-teachers in this class which are in other networks. + // In the future a firebase function should be watching for class doc creation + // and will update the networks. When that happens we can remove the networks + // property here and above. + await docRef.set({ ...aClass, network: addNetwork, networks: [addNetwork] }); + } else { + await docRef.set(aClass); + } + } + }); +} + +export async function syncClass(firestore: Firestore, rawPortalJWT: string|undefined, + aClass: ClassWithoutTeachers, user: UserModelType, addNetwork?: string) { + const { uri, context_id } = aClass; + const promises: Promise[] = []; + if (context_id) { + // Get list of teachers from the portal, if we have a portal login. + // Otherwise, default to just the current teacher (for demo/qa) + const teachers = (uri && rawPortalJWT) ? await getClassTeachers(uri, rawPortalJWT) : [user.id]; + if (!teachers) return; + const classWithTeachers = { ...aClass, teachers }; + if (addNetwork) { + classWithTeachers.network = addNetwork; + } + // Firestore will not accept 'undefined' values + if (classWithTeachers.network === undefined) { + delete classWithTeachers.network; + } + + // Old location of the class document + if (aClass.network) { + promises.push(createOrUpdateClassDoc(firestore, `classes/${aClass.network}_${context_id}`, + classWithTeachers, addNetwork)); + } + // New location of the class document + promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers, addNetwork)); } + return Promise.all(promises); } export async function syncOffering( diff --git a/src/models/commented-documents.test.ts b/src/models/commented-documents.test.ts new file mode 100644 index 0000000000..afb364621f --- /dev/null +++ b/src/models/commented-documents.test.ts @@ -0,0 +1,288 @@ +import fetchMock from "jest-fetch-mock"; +import "firebase/firestore"; +import { Firestore } from "../lib/firestore"; +import { UserModelType } from "./stores/user"; +import { CommentedDocumentsQuery } from "./commented-documents"; +import { DB } from "../lib/db"; + + +const mockStores = { + appMode: "authed", + demo: { name: "demo" }, + user: { portal: "test-portal" } +}; + +const mockDB = { + stores: mockStores +} as DB; + +const mockDocGet = jest.fn(); + +const mockDocSet = jest.fn(); + +const mockDocCollection = jest.fn(); + +const mockDocObject = { + get: mockDocGet, + set: (obj: any) => mockDocSet(obj), + collection: (path: string) => mockDocCollection(path) +}; +mockDocCollection.mockImplementation(() => mockCollectionObject); + +const mockDoc = jest.fn((path: string) => mockDocObject); + +const mockCollectionGet = jest.fn(); + +const mockCollectionWhere = jest.fn(); + +const mockCollectionObject = { + doc: mockDoc, + get: mockCollectionGet, + where: mockCollectionWhere +}; +mockCollectionObject.where.mockImplementation(() => mockCollectionObject); + +const mockCollection = jest.fn((path: string) => mockCollectionObject); + +jest.mock("firebase/app", () => ({ + firestore: () => ({ + collection: mockCollection, + doc: mockDoc, + runTransaction: jest.fn(callback => callback()) + }) +})); + +const user = { + id: "user-id", + network: "test-network" +} as UserModelType; + +const non_network_user = { + id: "user-id" +} as UserModelType; + +describe("CommentedDocumentsQuery", () => { + + let firestore: Firestore; + + function resetMocks() { + mockDoc.mockClear(); + mockCollection.mockClear(); + mockDocGet.mockReset(); + mockDocSet.mockClear(); + mockCollectionGet.mockReset(); + mockCollectionWhere.mockClear(); + fetchMock.resetMocks(); + } + + beforeEach(() => { + firestore = new Firestore(mockDB); + resetMocks(); + }); + + it("should return empty arrays if there are no documents", async () => { + mockCollectionGet.mockResolvedValue({ empty: true, docs: [] }); + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "problem-1"); + query.setUser(user); + + expect(mockCollectionGet).toHaveBeenCalledTimes(2); + expect(query.user).toEqual(user); + expect(query.curriculumDocs).toEqual([]); + expect(query.userDocs).toEqual([]); + }); + + it("should return empty arrays if there are curriculum documents with no comments", async () => { + const curriculumDocs = [ + { + id: "uid:user-id_unit-1_1_1_first", + data: () => { return { + uid: "user-id", + network: "test-network", + problem: "1.1", + section: "first", + unit: "unit-1" + }; } + }]; + mockCollectionGet + .mockResolvedValue({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, docs: curriculumDocs }); + // next call to get comments will return empty + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "1.1"); + query.setUser(user); + + expect(mockCollectionGet).toHaveBeenCalledTimes(2); + expect(mockCollectionWhere).toHaveBeenCalledWith("unit", "==", "unit-1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("problem", "==", "1.1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("network", "==", "test-network"); + + expect(query.user).toEqual(user); + expect(query.curriculumDocs).toEqual([]); + expect(query.userDocs).toEqual([]); + }); + + it("should find curriculum documents with comments for network user", async () => { + const curriculumDocs = [ + { + id: "uid:user-id_unit-1_1_1_first", + data: () => { return { + uid: "user-id", + network: "test-network", + problem: "1.1", + section: "first", + unit: "unit-1" + }; } + }]; + + mockCollectionGet + .mockResolvedValue({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, docs: curriculumDocs }) + .mockResolvedValueOnce({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, size: 2 }); + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "1.1"); + await query.setUser(user); + + // 4th query is for classes with teacher's network + expect(mockCollectionGet).toHaveBeenCalledTimes(4); + + expect(mockCollectionWhere).toHaveBeenCalledWith("unit", "==", "unit-1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("problem", "==", "1.1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("network", "==", "test-network"); + + expect(query.curriculumDocs).toEqual([ + { + id: "uid:user-id_unit-1_1_1_first", + network: "test-network", + unit: "unit-1", + problem: "1.1", + section: "first", + title: "Unknown", + uid: "user-id", + numComments: 2 + }]); + }); + + it("should find curriculum documents with comments for non-network user", async () => { + const curriculumDocs = [ + { + id: "uid:user-id_unit-1_1_1_first", + data: () => { return { + uid: "user-id", + network: null, + problem: "1.1", + section: "first", + unit: "unit-1" + }; } + }]; + + mockCollectionGet + .mockResolvedValue({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, docs: curriculumDocs }) + .mockResolvedValueOnce({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, size: 2 }); + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "1.1"); + await query.setUser(non_network_user); + + expect(mockCollectionGet).toHaveBeenCalledTimes(3); + + expect(mockCollectionWhere).toHaveBeenCalledWith("unit", "==", "unit-1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("problem", "==", "1.1"); + expect(mockCollectionWhere).toHaveBeenCalledWith("uid", "==", "user-id"); + + expect(query.curriculumDocs).toEqual([ + { + id: "uid:user-id_unit-1_1_1_first", + network: null, + unit: "unit-1", + problem: "1.1", + section: "first", + title: "Unknown", + uid: "user-id", + numComments: 2 + }]); + }); + + it("should return empty arrays if there are user documents with no comments", async () => { + const classList = [ + { data: () => ({ context_id: "class-1" }) } + ]; + + const userDocs = [ + { + id: "uid:user-id_unit-1_1_1_first", + data: () => { return { + uid: "user-id", + network: null, + problem: "1.1", + section: "first", + unit: "unit-1" + }; } + }]; + + mockCollectionGet + .mockResolvedValue({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: false, docs: classList }) + .mockResolvedValueOnce({ empty: false, docs: userDocs }); + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "1.1"); + await query.setUser(non_network_user); + + expect(mockCollectionGet).toHaveBeenCalledTimes(4); + + expect(mockCollectionWhere).toHaveBeenCalledWith("teachers", "array-contains", "user-id"); + expect(mockCollectionWhere).toHaveBeenCalledWith("context_id", "in", ["class-1"]); + + expect(query.curriculumDocs).toEqual([]); + expect(query.userDocs).toEqual([]); + }); + + it("should find user documents with comments for teacher's class", async () => { + const classList = [ + { data: () => ({ context_id: "class-1" }) } + ]; + + const userDocs = [ + { + id: "uid:document-id", + data: () => { return { + uid: "user-id", + type: "problem", + title: "doc-title", + createdAt: 1712951484070 + }; } + }]; + + mockCollectionGet + .mockResolvedValue({ empty: true, docs: [] }) + .mockResolvedValueOnce({ empty: true, docs: [] }) // curriculum docs query + .mockResolvedValueOnce({ empty: false, docs: classList }) + .mockResolvedValueOnce({ empty: false, docs: userDocs }) + .mockResolvedValueOnce({ empty: false, size: 2 }); + + const query = new CommentedDocumentsQuery(firestore, "unit-1", "1.1"); + await query.setUser(non_network_user); + + expect(mockCollectionGet).toHaveBeenCalledTimes(4); + + expect(mockCollectionWhere).toHaveBeenCalledWith("teachers", "array-contains", "user-id"); + expect(mockCollectionWhere).toHaveBeenCalledWith("context_id", "in", ["class-1"]); + + expect(query.curriculumDocs).toEqual([]); + expect(query.userDocs).toEqual([ + { + id: "uid:document-id", + uid: "user-id", + type: "problem", + title: "doc-title", + createdAt: 1712951484070, + numComments: 2 + } + ]); + }); + + +}); diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts new file mode 100644 index 0000000000..04147f9cee --- /dev/null +++ b/src/models/commented-documents.ts @@ -0,0 +1,147 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { chunk } from "lodash"; +import { Firestore } from "../lib/firestore"; +import { ClassDocument, CurriculumDocument, DocumentDocument } from "../lib/firestore-schema"; +import { getSectionTitle } from "./curriculum/section"; +import { UserModelType } from "./stores/user"; + +export interface CurriculumDocumentInfo { + id: string; + unit: string; + problem: string; + path: string; + title: string; + numComments: number; +} + +export interface UserDocumentInfo { + id: string; + key: string; + title: string; + numComments: number; +} + +export class CommentedDocumentsQuery { + db: Firestore; + user: UserModelType; + unit: string; + problem: string; + + curriculumDocs: CurriculumDocumentInfo[] = []; + userDocs: UserDocumentInfo[] = []; + + constructor( + db: Firestore, + unit: string, + problem: string) { + makeAutoObservable(this); + this.db = db; + this.unit = unit; + this.problem = problem; + } + + async setUser(user: UserModelType) { + this.user = user; + return Promise.all([ + this.queryCurriculumDocs(), + this.queryUserDocs()]); + } + + private async queryCurriculumDocs() { + const cDocsRef = this.db.collection("curriculum"); + let docsQuery; + if (this.user.network){ + docsQuery = cDocsRef + .where("unit", "==", this.unit) + .where("problem", "==", this.problem) + .where("network","==", this.user.network); + } else { + docsQuery = cDocsRef + .where("unit", "==", this.unit) + .where("problem", "==", this.problem) + //for teachers not in network, look for documents matching the uid + .where ("uid", "==", this.user.id); + } + const result = await docsQuery.get(); + const docs: CurriculumDocumentInfo[] = result.docs.map(doc => { + return { + id: doc.id, + title: "temp", + numComments: 0, + ...doc.data() as CurriculumDocument + }; + }); + const commentedDocs: CurriculumDocumentInfo[] = []; + const promiseArr: Promise[] = []; + for (let doc of docs) { + const docCommentsRef = cDocsRef.doc(doc.id).collection("comments"); + promiseArr.push(docCommentsRef.get().then((qs) => { + if (qs.empty === false) { + const firstCharPosition = doc.id.split("_", 4).join("_").length + 1; //first char after 4th _ + const sectionType = doc.id.substring(firstCharPosition, doc.id.length); + doc = { ...doc, title: getSectionTitle(sectionType), numComments: qs.size }; + commentedDocs.push(doc); + } + })); + } + await Promise.all(promiseArr); + runInAction(() => { + this.curriculumDocs = commentedDocs; + }); + } + + private async queryUserDocs() { + // Find teacher's classes + const classesRef = this.db.collection("classes"); + const individualClasses = (await classesRef.where("teachers", "array-contains", this.user.id).get()).docs; + const networkClasses = this.user.network + ? (await classesRef.where("networks", "array-contains", this.user.network).get()).docs + : []; + const allClasses = individualClasses.concat(networkClasses); + const classIds = new Set(); + allClasses.forEach(doc => { + classIds.add((doc.data() as ClassDocument).context_id); + }); + + // Find student documents + if (classIds.size === 0) { + return; + } + const collection = this.db.collection("documents"); + // Firestore has a limit of ~10 for "in" queries (30 in recent versions), so we need to iterate over the classes + const chunkSize = 10; + const teacherClassGroups = chunk([...classIds], chunkSize); + const studentDocs: UserDocumentInfo[] = []; + for (const group of teacherClassGroups) { + const docsQuery = collection.where("context_id", "in", group); + const result = await docsQuery.get(); + for (const doc of result.docs) { + const data = doc.data() as DocumentDocument; + studentDocs.push({ + id: doc.id, + title: "temp", + numComments: 0, + ...data + }); + } + } + const commentedDocs: UserDocumentInfo[] = []; + const promiseArr: Promise[] = []; + // TODO maybe combine multiple "docs" that have same ID? + for (const doc of studentDocs){ + const docCommentsRef = collection.doc(doc.id).collection("comments"); + // NOTE, Firestore v10 supports `.count()` queries so we wouldn't have to fetch the entire collection + promiseArr.push(docCommentsRef.get().then((qs)=>{ + if (qs.empty === false){ + const commentedDoc = {...doc, numComments: qs.size}; + commentedDocs.push(commentedDoc); + } + })); + } + await Promise.all(promiseArr); + runInAction(() => { + this.userDocs = commentedDocs; + }); + } + +} diff --git a/src/models/curriculum/log-curriculum-event.ts b/src/models/curriculum/log-curriculum-event.ts index 3f40f10b4b..cec33716e8 100644 --- a/src/models/curriculum/log-curriculum-event.ts +++ b/src/models/curriculum/log-curriculum-event.ts @@ -1,4 +1,4 @@ -import { parseSectionPath } from "../../../functions/src/shared"; +import { parseSectionPath } from "../../../shared/shared"; import { Logger } from "../../lib/logger"; import { LogEventName } from "../../lib/logger-types"; diff --git a/src/models/curriculum/section.ts b/src/models/curriculum/section.ts index 64797c51db..1adfedce77 100644 --- a/src/models/curriculum/section.ts +++ b/src/models/curriculum/section.ts @@ -1,5 +1,5 @@ import { IAnyStateTreeNode, Instance, SnapshotIn, types } from "mobx-state-tree"; -import { parseSectionPath } from "../../../functions/src/shared"; +import { parseSectionPath } from "../../../shared/shared"; import { DocumentContentModel } from "../document/document-content"; import { IAuthoredTileContent } from "../document/document-content-import-types"; import { SupportModel } from "./support"; diff --git a/src/models/curriculum/unit.ts b/src/models/curriculum/unit.ts index 8ba908fa42..ffe8aabedf 100644 --- a/src/models/curriculum/unit.ts +++ b/src/models/curriculum/unit.ts @@ -1,7 +1,7 @@ import { IReactionDisposer, reaction } from "mobx"; import { getParent, Instance, SnapshotIn, types } from "mobx-state-tree"; -import { buildProblemPath, buildSectionPath } from "../../../functions/src/shared"; +import { buildProblemPath, buildSectionPath } from "../../../shared/shared"; import { DocumentContentModel } from "../document/document-content"; import { InvestigationModel, InvestigationModelType } from "./investigation"; import { ISectionInfoMap, SectionModel, SectionModelType,registerSectionInfo } from "./section"; diff --git a/src/models/document/base-document-content.ts b/src/models/document/base-document-content.ts index 86ccee6232..a3342248b9 100644 --- a/src/models/document/base-document-content.ts +++ b/src/models/document/base-document-content.ts @@ -258,6 +258,9 @@ export const BaseDocumentContentModel = types }); return tiles; }, + get tileTypes() { + return new Set(Array.from(self.tileMap.values()).map(tile => tile.content.type)); + }, getTilesOfType(type: string) { const tiles: string[] = []; const lcType = type.toLowerCase(); diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index 018e5c48cb..0712c82db8 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -1,22 +1,30 @@ import { getParent } from "mobx-state-tree"; +import { IDocumentMetadata } from "../../../shared/shared"; import { ProblemModelType } from "../curriculum/problem"; import { SectionModelType } from "../curriculum/section"; import { getSectionPath } from "../curriculum/unit"; import { AppConfigModelType } from "../stores/app-config-model"; -import { DocumentModelType } from "./document"; +import { UserModelType } from "../stores/user"; +import { DocumentModelType, IExemplarVisibilityProvider } from "./document"; import { DocumentContentModelType } from "./document-content"; -import { isPlanningType, isProblemType } from "./document-types"; +import { isExemplarType, isPlanningType, isProblemType, isPublishedType } from "./document-types"; export function getDocumentDisplayTitle( - document: DocumentModelType, appConfig: AppConfigModelType, problem: ProblemModelType + document: DocumentModelType, appConfig: AppConfigModelType, problem?: ProblemModelType, + unit?: string ) { const { type } = document; + const documentProblemOrdinal = `${document.investigation}.${document.problem}`; + const problemTitle = !(document.problem || document.investigation || document.unit) || + (documentProblemOrdinal === String(problem?.ordinal) && unit === document?.unit) + ? problem?.title || "Unknown Problem" + : "Unknown Problem"; return document.isSupport ? document.getProperty("caption") || "Support" : isProblemType(type) - ? problem.title + ? problemTitle : isPlanningType(type) - ? `${problem.title}: Planning` + ? `${problem?.title || "Unkown"}: Planning` : document.getDisplayTitle(appConfig); } @@ -38,3 +46,17 @@ export function getDocumentIdentifier(document?: DocumentContentModelType) { return getSectionPath(section); } } + +export const isDocumentAccessibleToUser = ( + doc: IDocumentMetadata, user: UserModelType, documentStore: IExemplarVisibilityProvider +) => { + const ownDocument = doc.uid === user.id; + const isShared = doc.visibility === "public"; + const isPublished = isPublishedType(doc.type); + if (user.type === "teacher") return true; + if (user.type === "student") { + return ownDocument || isShared || isPublished + || (isExemplarType(doc.type) && documentStore.isExemplarVisible(doc.key)); + } + return false; +}; diff --git a/src/models/document/document.test.ts b/src/models/document/document.test.ts index ae47c0b88a..ead94cf266 100644 --- a/src/models/document/document.test.ts +++ b/src/models/document/document.test.ts @@ -275,7 +275,8 @@ describe("document model", () => { uid: "1", key: "test", createdAt: 1, - properties: {} + properties: {}, + visibility: "public" }); }); diff --git a/src/models/document/document.ts b/src/models/document/document.ts index d30c344c18..28b39b3abf 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -3,8 +3,7 @@ import { forEach } from "lodash"; import { QueryClient, UseQueryResult } from "react-query"; import { DocumentContentModel, DocumentContentSnapshotType } from "./document-content"; import { IDocumentAddTileOptions } from "./document-content-types"; -import { DocumentTypeEnum, IDocumentContext, ISetProperties, - isExemplarType, +import { DocumentTypeEnum, IDocumentContext, ISetProperties, isPublishedType, LearningLogDocument, LearningLogPublication, PersonalDocument, PersonalPublication, PlanningDocument, ProblemDocument, ProblemPublication, SupportPublication } from "./document-types"; @@ -13,7 +12,7 @@ import { TileCommentsModel, TileCommentsModelType } from "../tiles/tile-comments import { getSharedModelManager, getTileEnvironment } from "../tiles/tile-environment"; import { IDocumentMetadata, IGetNetworkDocumentParams, IGetNetworkDocumentResponse, IUserContext -} from "../../../functions/src/shared"; +} from "../../../shared/shared"; import { getFirebaseFunction } from "../../hooks/use-firebase-function"; import { IDocumentProperties } from "../../lib/db-types"; import { getLocalTimeStamp } from "../../utilities/time"; @@ -27,13 +26,14 @@ import { ESupportType } from "../curriculum/support"; import { IDocumentLogEvent, logDocumentEvent } from "./log-document-event"; import { LogEventMethod, LogEventName } from "../../lib/logger-types"; import { UserModelType } from "../stores/user"; +import { isDocumentAccessibleToUser } from "./document-utils"; export enum ContentStatus { Valid, Error } -type IExemplarVisibilityProvider = { +export type IExemplarVisibilityProvider = { isExemplarVisible: (id: string) => boolean; }; @@ -54,7 +54,10 @@ export const DocumentModel = Tree.named("Document") originDoc: types.maybe(types.string), changeCount: types.optional(types.number, 0), pubVersion: types.maybe(types.number), - supportContentType: types.maybe(types.enumeration("SupportType", Object.values(ESupportType))) + supportContentType: types.maybe(types.enumeration("SupportType", Object.values(ESupportType))), + problem: types.maybe(types.string), + investigation: types.maybe(types.string), + unit: types.maybe(types.string), }) .volatile(self => ({ treeMonitor: undefined as TreeMonitor | undefined, @@ -84,10 +87,7 @@ export const DocumentModel = Tree.named("Document") return self.type === SupportPublication; }, get isPublished() { - return (self.type === ProblemPublication) - || (self.type === LearningLogPublication) - || (self.type === PersonalPublication) - || (self.type === SupportPublication); + return isPublishedType(self.type); }, get isRemote() { return !!self.remoteContext; @@ -101,14 +101,15 @@ export const DocumentModel = Tree.named("Document") return !!self.content; }, get metadata(): IDocumentMetadata { - const { uid, type, key, createdAt, title, originDoc, properties } = self; + const { uid, type, key, createdAt, title, originDoc, properties, visibility } = self; // FIXME: the contextId was added here temporarily. This metadata is sent // up to the Firestore functions. The new functions do not require the // contextId. However the old functions do. The old functions were just // ignoring this contextId. So the contextId is added here so the client // code can work with the old functions. return { contextId: "ignored", uid, type, key, createdAt, title, - originDoc, properties: properties.toJSON() } as IDocumentMetadata; + originDoc, properties: properties.toJSON(), investigation: self.investigation, + problem: self.problem, unit: self.unit, visibility } as IDocumentMetadata; }, getProperty(key: string) { return self.properties.get(key); @@ -157,14 +158,7 @@ export const DocumentModel = Tree.named("Document") return self.content?.getUniqueTitleForType(tileType); }, isAccessibleToUser(user: UserModelType, documentStore: IExemplarVisibilityProvider) { - const ownDocument = self.uid === user.id; - const isShared = self.visibility === "public"; - if (user.type === "teacher") return true; - if (user.type === "student") { - return ownDocument || isShared || self.isPublished - || (isExemplarType(self.type) && documentStore.isExemplarVisible(self.key)); - } - return false; + return isDocumentAccessibleToUser(self.metadata, user, documentStore); } })) .actions((self) => ({ diff --git a/src/models/document/log-document-event.ts b/src/models/document/log-document-event.ts index f0bfed98e1..c0a1404e55 100644 --- a/src/models/document/log-document-event.ts +++ b/src/models/document/log-document-event.ts @@ -1,3 +1,4 @@ +import { IDocumentMetadata } from "../../../shared/shared"; import { Logger } from "../../lib/logger"; import { LogEventMethod, LogEventName } from "../../lib/logger-types"; import { UserModelType } from "../stores/user"; @@ -10,7 +11,7 @@ interface ITeacherNetworkInfo { } export interface IDocumentLogEvent extends Record { - document: DocumentModelType; + document: DocumentModelType | IDocumentMetadata; } export function isDocumentLogEvent(params: Record): params is IDocumentLogEvent { @@ -23,8 +24,16 @@ interface IContext extends Record { function processDocumentEventParams(params: IDocumentLogEvent, { user }: IContext) { const { document, ...others } = params; - const teacherNetworkInfo: ITeacherNetworkInfo | undefined = document.isRemote - ? { networkClassHash: document.remoteContext, + const isRemote = "isRemote" in document ? document.isRemote : undefined; + const remoteContext = "remoteContext" in document ? document.remoteContext : undefined; + const documentProperties = document.properties && typeof document.properties.toJSON === "function" + ? document.properties.toJSON() + : {}; + const documentVisibility = "visibility" in document ? document.visibility : undefined; + const documentChanges = "changeCount" in document ? document.changeCount : undefined; + + const teacherNetworkInfo: ITeacherNetworkInfo | undefined = isRemote + ? { networkClassHash: remoteContext, networkUsername: `${document.uid}@${user.portal}` } : undefined; @@ -33,9 +42,9 @@ function processDocumentEventParams(params: IDocumentLogEvent, { user }: IContex documentKey: document.key, documentType: document.type, documentTitle: document.title || "", - documentProperties: document.properties.toJSON(), - documentVisibility: document.visibility, - documentChanges: document.changeCount, + documentProperties, + documentVisibility, + documentChanges, ...others, ...teacherNetworkInfo }; @@ -50,11 +59,12 @@ export function logDocumentEvent(event: LogEventName, _params: IDocumentLogEvent * Convenience function to log the appropriate type of VIEW_SHOW_*_DOCUMENT event for the document. * @param document */ -export function logDocumentViewEvent(document: DocumentModelType) { +export function logDocumentViewEvent(document: DocumentModelType | IDocumentMetadata) { + const isRemote = "isRemote" in document ? document.isRemote : undefined; const event = document.type === ExemplarDocument ? LogEventName.VIEW_SHOW_EXEMPLAR_DOCUMENT - : document.isRemote + : isRemote ? LogEventName.VIEW_SHOW_TEACHER_NETWORK_COMPARISON_DOCUMENT : LogEventName.VIEW_SHOW_COMPARISON_DOCUMENT; logDocumentEvent(event, { document }); diff --git a/src/models/history/log-history-event.ts b/src/models/history/log-history-event.ts index 1465b8c535..28ccccaeab 100644 --- a/src/models/history/log-history-event.ts +++ b/src/models/history/log-history-event.ts @@ -1,4 +1,4 @@ -import { isSectionPath } from "../../../functions/src/shared"; +import { isSectionPath } from "../../../shared/shared"; import { Logger } from "../../lib/logger"; import { LogEventName } from "../../lib/logger-types"; import { isCurriculumLogEvent, logCurriculumEvent } from "../curriculum/log-curriculum-event"; diff --git a/src/models/history/tree-manager.ts b/src/models/history/tree-manager.ts index 38286d21b4..667e88ce8a 100644 --- a/src/models/history/tree-manager.ts +++ b/src/models/history/tree-manager.ts @@ -9,8 +9,7 @@ import { TreePatchRecord, HistoryEntry, TreePatchRecordSnapshot, HistoryOperation } from "./history"; import { DEBUG_HISTORY } from "../../lib/debug"; import { getFirebaseFunction } from "../../hooks/use-firebase-function"; -import { ICommentableDocumentParams, IDocumentMetadata, - networkDocumentKey } from "../../../functions/src/shared"; +import { getDocumentPath, ICommentableDocumentParams, IDocumentMetadata } from "../../../shared/shared"; import { Firestore } from "../../lib/firestore"; import { UserModelType } from "../stores/user"; import { UserContextProvider } from "../stores/user-context-provider"; @@ -644,9 +643,3 @@ async function prepareFirestoreHistoryInfo( lastEntryId: lastHistoryEntry?.id || null }; } - -function getDocumentPath(userId: string, documentKey: string, network?: string) { - const networkDocKey = networkDocumentKey(userId, documentKey, network); - const documentPath = `documents/${networkDocKey}`; - return documentPath; -} diff --git a/src/models/image-map.test.ts b/src/models/image-map.test.ts index 9831a3be41..1c79e4be12 100644 --- a/src/models/image-map.test.ts +++ b/src/models/image-map.test.ts @@ -1,6 +1,6 @@ import { autorun, flowResult, runInAction, when } from "mobx"; -import { parseFirebaseImageUrl } from "../../functions/src/shared-utils"; +import { parseFirebaseImageUrl } from "../../shared/shared-utils"; import { DB } from "../lib/db"; import * as ImageUtils from "../utilities/image-utils"; import placeholderImage from "../assets/image_placeholder.png"; diff --git a/src/models/stores/create-exemplar-docs.ts b/src/models/stores/create-exemplar-docs.ts index a1182bd559..a9a81fc70f 100644 --- a/src/models/stores/create-exemplar-docs.ts +++ b/src/models/stores/create-exemplar-docs.ts @@ -7,9 +7,13 @@ import { kExemplarUserParams } from "./user-types"; import { ICurriculumConfig } from "./curriculum-config"; import { ExemplarDocument } from "../document/document-types"; import { AppConfigModelType } from "./app-config-model"; +import { UnitModelType } from "../curriculum/unit"; +import { InvestigationModelType } from "../curriculum/investigation"; interface ICreateExemplarDocsParams { + unit: UnitModelType; unitUrl: string; + investigation?: InvestigationModelType; problem: ProblemModelType; documents: DocumentsModelType; classStore: ClassModelType; @@ -29,7 +33,9 @@ interface IExemplarData { // plus a second paramter for the unitUrl // function would only require the properties it needs export async function createAndLoadExemplarDocs({ + unit, unitUrl, + investigation, problem, documents, classStore, @@ -39,7 +45,7 @@ export async function createAndLoadExemplarDocs({ const { exemplars } = problem; const exemplarsData = await getExemplarsData(unitUrl, exemplars); classStore.addUser(ClassUserModel.create(kExemplarUserParams)); - createExemplarDocs(documents, exemplarsData, curriculumConfig, appConfig); + createExemplarDocs(unit, investigation, problem, documents, exemplarsData, curriculumConfig, appConfig); } export async function getExemplarsData(unitUrl: string, exemplarUrls: string[]){ @@ -67,6 +73,9 @@ export function createExemplarDocId(exemplarDataUrl: string, curriculumBaseUrl: } function createExemplarDocs( + unit: UnitModelType, + investigation: InvestigationModelType | undefined, + problem: ProblemModelType, documents: DocumentsModelType, exemplarsData: IExemplarData[], curriculumConfig: ICurriculumConfig, @@ -84,7 +93,10 @@ function createExemplarDocs( key: exemplarDocId, properties: { authoredCommentTag: exemplarData.tag - } + }, + unit: unit.code, + investigation: investigation?.ordinal.toString(), + problem: problem.ordinal.toString() }; const newDoc = createDocumentModelWithEnv(appConfig, newDocParams); documents.add(newDoc); diff --git a/src/models/stores/document-group.test.ts b/src/models/stores/document-group.test.ts new file mode 100644 index 0000000000..598c33899c --- /dev/null +++ b/src/models/stores/document-group.test.ts @@ -0,0 +1,331 @@ +import { IObservableArray, observable } from "mobx"; +import { createDocumentModel, DocumentModelSnapshotType, DocumentModelType } from "../document/document"; +import { DocumentContentSnapshotType } from "../document/document-content"; +import { ProblemDocument } from '../document/document-types'; +import { ClassModel, ClassModelType, ClassUserModel } from "./class"; +import { GroupModel, GroupsModel, GroupsModelType, GroupUserModel } from "./groups"; +import { DeepPartial } from "utility-types"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { ISortedDocumentsStores, SortedDocuments } from "./sorted-documents"; +import { DB } from "../../lib/db"; +import { mock } from "ts-jest-mocker"; +import { Bookmark, Bookmarks } from "./bookmarks"; + + +//****************************************** Documents Mock *************************************** + +const mockDocumentsData: DocumentModelSnapshotType[] = [ + { uid: "1", //Joe + type: ProblemDocument, key:"Student 1 Problem Doc Group 5", groupId: "5", createdAt: 1, + content: { tiles: [] } as DocumentContentSnapshotType + }, + { uid: "2", //Scott + type: ProblemDocument, key:"Student 2 Problem Doc Group 3", groupId: "3", createdAt: 2, + content: { tiles: [{ id: "textTool", content: {type: "Text" }}] } as DocumentContentSnapshotType + }, + { uid: "3", //Dennis + type: ProblemDocument, key:"Student 3 Problem Doc Group 9", groupId: "9", createdAt: 3, + content: { tiles: [ + { id: "drawingTool", content: { type: "Drawing", objects: [] }}] } as DocumentContentSnapshotType + }, + { uid: "4", //Kirk + type: ProblemDocument, key:"Student 4 Problem Doc Group 3", groupId: "3", createdAt: 4, + content: { tiles: [] } as DocumentContentSnapshotType + } +]; + +const mockMetadataDocuments: IObservableArray = observable.array([ + { + uid: "1", //Joe + type: ProblemDocument, + key:"Student 1 Problem Doc Group 5", + createdAt: 1, + tools: [], + strategies: ["foo", "bar"], + }, + { + uid: "2", //Scott + type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, + tools: ["Text"] + }, + { + uid: "3", //Dennis + type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, + tools: ["Drawing"] + }, + { + uid: "4", //Kirk + type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, + tools: [], + strategies: ["bar"] + } +]); + +const createMockDocuments = () => { + return mockDocumentsData.map(createDocumentModel); +}; + +//**************************************** Class/Users Mock *************************************** + +const createMockClassUsers = () => { + return { + "1": ClassUserModel.create( + { type: "student", id: "1", firstName: "Joe", lastName: "Bacal", + fullName: "Joe Bacal", initials: "JB" }), + "2": ClassUserModel.create( + { type: "student", id: "2", firstName: "Scott", lastName: "Cytacki", + fullName: "Scott Cytacki", initials: "SC" }), + "3": ClassUserModel.create( + { type: "student", id: "3", firstName: "Dennis", lastName: "Cao", + fullName: "Dennis Cao", initials: "DC" }), + "4": ClassUserModel.create( + { type: "student", id: "4", firstName: "Kirk", lastName: "Swenson", + fullName: "Kirk Swenson", initials: "KS" }), + }; +}; +const createMockClassWithUsers = () => { + const mockUsers = createMockClassUsers(); + const mockClass = ClassModel.create({ + name: "Mock Class", + classHash: "mock", + users: mockUsers + }); + return mockClass; +}; + +//****************************************** Groups Mock ****************************************** + +type GroupUserData = { + id: string; + name: string; + initials: string; + connectedTimestamp: number; + disconnectedTimestamp?: number; +}; + +const createMockGroupUsers = (groupUsersData: GroupUserData[]) => { + return groupUsersData.map(userData => + GroupUserModel.create({ + id: userData.id, + name: userData.name, + initials: userData.initials, + connectedTimestamp: userData.connectedTimestamp, + disconnectedTimestamp: userData.disconnectedTimestamp + }) + ); +}; + +const createMockGroups = () => { + const group3UsersData = [ + { id: "2", name: "Scott Cytacki", initials: "SC", connectedTimestamp: 2 }, + { id: "4", name: "Kirk Swenson", initials: "KS", connectedTimestamp: 4 }, + ]; + const group5UsersData = [ + { id: "1", name: "Joe Bacal", initials: "JB", connectedTimestamp: 1 }, + ]; + const group9UsersData = [ + { id: "3", name: "Dennis Cao", initials: "DC", connectedTimestamp: 3 }, + ]; + + const group3Users = createMockGroupUsers(group3UsersData); + const group5Users = createMockGroupUsers(group5UsersData); + const group9Users = createMockGroupUsers(group9UsersData); + + const mockGroups = GroupsModel.create({ + allGroups: [ + GroupModel.create({ id: "3", users: group3Users }), + GroupModel.create({ id: "5", users: group5Users }), + GroupModel.create({ id: "9", users: group9Users }), + ] + }); + return mockGroups; +}; + +// ***** Bookmarks Mock ***** // + +function addDocBookmarks(bookmarks: Bookmarks, bookmarkMap: Record>) { + Object.entries(bookmarkMap).forEach(([docKey, array]) => { + bookmarks.bookmarkMap.set(docKey, observable.array(array)); + }); +} + +//****************************************** Jest Tests ******************************************* + + +describe('DocumentGroup Model', () => { + let sortedDocuments: SortedDocuments; + let mockDocuments: DocumentModelType[]; + let mockGroups: GroupsModelType; + let mockClass: ClassModelType; + let bookmarks: Bookmarks; + + beforeEach(() => { + mockDocuments = createMockDocuments(); + mockGroups = createMockGroups(); + mockClass = createMockClassWithUsers(); + const db = mock(DB); + Object.setPrototypeOf(db, DB); + bookmarks = new Bookmarks({db}); + + const mockStores: DeepPartial = { + //DeepPartial allows us to not need to mock the "dB" and "appConfig" stores + //as well not needing to type the stores below + documents: { all: mockDocuments }, + groups: mockGroups, + class: mockClass, + appConfig: { commentTags: {"foo": "foo", "bar": "bar"} }, + bookmarks + }; + + sortedDocuments = new SortedDocuments(mockStores as ISortedDocumentsStores); + sortedDocuments.firestoreMetadataDocs = mockMetadataDocuments; + }); + + describe("byBookMarked Function", () => { + it('should return a doc collection sorted by bookmarks and with the correct documents per bookmark', () => { + addDocBookmarks(bookmarks, { + ["Student 2 Problem Doc Group 3"]: [ + new Bookmark("1", "a", true), + ], + ["Student 1 Problem Doc Group 5"]: [ + new Bookmark("1", "a", true), + new Bookmark("2", "b", true), + ] + }); + + const documentsByGroup = sortedDocuments.byGroup; + const documentCollection = documentsByGroup[0].byBookmarked; + expect(documentCollection.length).toBe(2); + expect(documentCollection[0].label).toBe("Bookmarked"); + expect(documentCollection[0].documents.length).toBe(1); + expect(documentCollection[1].label).toBe("Not Bookmarked"); + expect(documentCollection[1].documents.length).toBe(1); + + const documentCollection2 = documentsByGroup[1].byBookmarked; + expect(documentCollection2.length).toBe(2); + expect(documentCollection2[0].label).toBe("Bookmarked"); + expect(documentCollection2[0].documents.length).toBe(1); + expect(documentCollection2[1].label).toBe("Not Bookmarked"); + expect(documentCollection2[1].documents.length).toBe(0); + + const documentCollection3 = documentsByGroup[2].byBookmarked; + expect(documentCollection3.length).toBe(2); + expect(documentCollection3[0].label).toBe("Bookmarked"); + expect(documentCollection3[0].documents.length).toBe(0); + expect(documentCollection3[1].label).toBe("Not Bookmarked"); + expect(documentCollection3[1].documents.length).toBe(1); + }); + }); + + describe("byGroup Function", () => { + it('should return a document collection sorted by group names and with the correct documents per group', () => { + const expectedGroups = [ + { label: "Group 5", index: 0 }, + { label: "Group 9", index: 1 }, + { label: "Group 3", index: 2 }, + { label: "Group 3", index: 3 } + ]; + expectedGroups.forEach(({ label, index }) => { + const documentGroup = sortedDocuments.byName[index]; + const documentCollection = documentGroup.byGroup; + expect(documentCollection.length).toBe(1); + expect(documentCollection[0].label).toBe(label); + expect(documentCollection[0].documents.length).toBe(1); + }); + }); + + }); + + describe("byName Function", () => { + it ('should return a document collection alphabetized by last name with the correct documents per user', () => { + const documentGroup = sortedDocuments.byGroup[0]; + const documentCollection = documentGroup.byName; + expect(documentCollection.length).toBe(2); + expect(documentCollection[0].label).toBe("Cytacki, Scott"); + expect(documentCollection[0].documents.length).toBe(1); + expect(documentCollection[1].label).toBe("Swenson, Kirk"); + expect(documentCollection[1].documents.length).toBe(1); + + const documentGroup2 = sortedDocuments.byGroup[1]; + const documentCollection2 = documentGroup2.byName; + expect(documentCollection2.length).toBe(1); + expect(documentCollection2[0].label).toBe("Bacal, Joe"); + expect(documentCollection2[0].documents.length).toBe(1); + + const documentGroup3 = sortedDocuments.byGroup[2]; + const documentCollection3 = documentGroup3.byName; + expect(documentCollection3.length).toBe(1); + expect(documentCollection3[0].label).toBe("Cao, Dennis"); + expect(documentCollection3[0].documents.length).toBe(1); + }); + }); + + describe("byStrategy Function", () => { + it('should return a document collection sorted by strategy with the correct documents per strategy', () => { + const documentGroup = sortedDocuments.byName[0]; + const documentCollection = documentGroup.byStrategy; + expect(documentCollection.length).toBe(3); // 'Not Tagged' is added by default to the list of strategies + expect(documentCollection[0].label).toBe("foo"); + expect(documentCollection[0].documents.length).toBe(1); + expect(documentCollection[1].label).toBe("bar"); + expect(documentCollection[1].documents.length).toBe(1); + expect(documentCollection[2].label).toBe("Not Tagged"); + expect(documentCollection[2].documents.length).toBe(0); + + const documentGroup2 = sortedDocuments.byName[1]; + const documentCollection2 = documentGroup2.byStrategy; + expect(documentCollection2.length).toBe(3); + expect(documentCollection2[0].label).toBe("foo"); + expect(documentCollection2[0].documents.length).toBe(0); + expect(documentCollection2[1].label).toBe("bar"); + expect(documentCollection2[1].documents.length).toBe(0); + expect(documentCollection2[2].label).toBe("Not Tagged"); + expect(documentCollection2[2].documents.length).toBe(1); + + const documentGroup3 = sortedDocuments.byName[2]; + const documentCollection3 = documentGroup3.byStrategy; + expect(documentCollection3.length).toBe(3); + expect(documentCollection3[0].label).toBe("foo"); + expect(documentCollection3[0].documents.length).toBe(0); + expect(documentCollection3[1].label).toBe("bar"); + expect(documentCollection3[1].documents.length).toBe(0); + expect(documentCollection3[2].label).toBe("Not Tagged"); + expect(documentCollection3[2].documents.length).toBe(1); + + const documentGroup4 = sortedDocuments.byName[3]; + const documentCollection4 = documentGroup4.byStrategy; + expect(documentCollection4.length).toBe(3); + expect(documentCollection4[0].label).toBe("foo"); + expect(documentCollection4[0].documents.length).toBe(0); + expect(documentCollection4[1].label).toBe("bar"); + expect(documentCollection4[1].documents.length).toBe(1); + expect(documentCollection4[2].label).toBe("Not Tagged"); + expect(documentCollection4[2].documents.length).toBe(0); + }); + }); + + describe("byTools Function", () => { + it ('should return a document collection sorted by tool with the correct documents per tool', () => { + const documentGroup = sortedDocuments.byGroup[0]; + const documentCollection = documentGroup.byTools; + expect(documentCollection.length).toBe(2); + expect(documentCollection[0].label).toBe("Text"); + expect(documentCollection[0].documents.length).toBe(1); + expect(documentCollection[1].label).toBe("No Tools"); + expect(documentCollection[1].documents.length).toBe(1); + + const documentGroup2 = sortedDocuments.byGroup[1]; + const documentCollection2 = documentGroup2.byTools; + expect(documentCollection2.length).toBe(1); + expect(documentCollection2[0].label).toBe("No Tools"); + expect(documentCollection2[0].documents.length).toBe(1); + + const documentGroup3 = sortedDocuments.byGroup[2]; + const documentCollection3 = documentGroup3.byTools; + expect(documentCollection3.length).toBe(1); + expect(documentCollection3[0].label).toBe("Drawing"); + expect(documentCollection3[0].documents.length).toBe(1); + }); + }); + +}); diff --git a/src/models/stores/document-group.ts b/src/models/stores/document-group.ts new file mode 100644 index 0000000000..24db168fdd --- /dev/null +++ b/src/models/stores/document-group.ts @@ -0,0 +1,154 @@ +import { FC, SVGProps } from "react"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { ISortedDocumentsStores, TagWithDocs } from "./sorted-documents"; +import { makeAutoObservable } from "mobx"; +import { + createDocMapByBookmarks, + createDocMapByGroups, + createDocMapByNames, + createTileTypeToDocumentsMap, + getTagsWithDocs, + sortGroupSectionLabels, + sortNameSectionLabels +} from "../../utilities/sort-document-utils"; +import { getTileContentInfo } from "../tiles/tile-content-info"; +import { getTileComponentInfo } from "../tiles/tile-component-info"; +import { SecondarySortType } from "./ui-types"; + +import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; + +interface IDocumentGroup { + icon?:FC>; + label: string; + documents: IDocumentMetadata[]; + stores: ISortedDocumentsStores; +} + +/* + * DocumentGroup + * + * Represents a collection of related metadata documents, allowing for various + * sorting options to organize these documents based on different criteria. + * + * Provides methods to sort and group documents by: + * - Groups + * - Names + * - Strategies + * - Tools + * - Bookmarks + * + * Its main purpose is to provide sub sorting options for documents that are already + * sorted by a primary sort filter. + * + */ +export class DocumentGroup { + stores: ISortedDocumentsStores; + label: string; + documents: IDocumentMetadata[]; + firestoreTagDocumentMap = new Map>(); + icon?: FC>; + + constructor(props: IDocumentGroup) { + makeAutoObservable(this); + const { stores, label, documents, icon } = props; + this.stores = stores; + this.label = label; + this.documents = documents; + this.icon = icon; + } + + buildDocumentCollection(sortedSectionLabels: string[], docMap: Map): DocumentGroup[] { + return sortedSectionLabels.map(label => { + return new DocumentGroup({ + label, + documents: docMap.get(label) ?? [], + stores: this.stores + }); + }); + } + + sortBy(sortType: SecondarySortType): DocumentGroup[] { + switch (sortType) { + case "Group": + return this.byGroup; + case "Name": + return this.byName; + case "Strategy": + return this.byStrategy; + case "Tools": + return this.byTools; + case "Bookmarked": + return this.byBookmarked; + default: + return []; + } + } + + get byGroup(): DocumentGroup[] { + const docMap = createDocMapByGroups(this.documents, this.stores.groups.groupForUser); + const sortedSectionLabels = sortGroupSectionLabels(Array.from(docMap.keys())); + return this.buildDocumentCollection(sortedSectionLabels, docMap); + } + + get byName(): DocumentGroup[] { + const docMap = createDocMapByNames(this.documents, this.stores.class.getUserById); + const sortedSectionLabels = sortNameSectionLabels(Array.from(docMap.keys())); + return this.buildDocumentCollection(sortedSectionLabels, docMap); + } + + get byStrategy(): DocumentGroup[] { + const commentTags = this.stores.appConfig.commentTags; + const tagsWithDocs = getTagsWithDocs(this.documents, commentTags, this.firestoreTagDocumentMap); + + const sortedDocsArr: DocumentGroup[] = []; + Object.entries(tagsWithDocs).forEach((tagKeyAndValObj) => { + const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; + const label = tagWithDocs.tagValue; + const docKeys = tagWithDocs.docKeysFoundWithTag; + const documents = this.documents.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); + sortedDocsArr.push(new DocumentGroup({ + label, + documents, + stores: this.stores + })); + }); + return sortedDocsArr; + } + + get byTools(): DocumentGroup[] { + const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.documents); + + // Map the tile types to their display names + const sectionedDocuments = Array.from(tileTypeToDocumentsMap.keys()).map(tileType => { + const section: DocumentGroup = new DocumentGroup({ + label: tileType, + documents: tileTypeToDocumentsMap.get(tileType)?.documents ?? [], + stores: this.stores + }); + if (tileType === "Sparrow") { + section.icon = SparrowHeaderIcon; + } else { + const contentInfo = getTileContentInfo(tileType); + section.label = contentInfo?.displayName || tileType; + const componentInfo = getTileComponentInfo(tileType); + section.icon = componentInfo?.HeaderIcon; + } + return section; + }); + + // Sort the tile types. 'No Tools' should be at the end. + const sortedByLabel = sectionedDocuments.sort((a, b) => { + if (a.label === "No Tools") return 1; // Move 'No Tools' to the end + if (b.label === "No Tools") return -1; // Alphabetically sort all others + return a.label.localeCompare(b.label); + }); + + return sortedByLabel; + } + + get byBookmarked(): DocumentGroup[] { + const docMap = createDocMapByBookmarks(this.documents, this.stores.bookmarks); + const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; + return this.buildDocumentCollection(sortedSectionLabels, docMap); + } +} diff --git a/src/models/stores/documents.ts b/src/models/stores/documents.ts index 1a7aa7ed74..fbf15066b0 100644 --- a/src/models/stores/documents.ts +++ b/src/models/stores/documents.ts @@ -50,7 +50,6 @@ export const DocumentsModel = types getDocument(documentKey: string) { return self.all.find((document) => document.key === documentKey); }, - byType(type: DocumentType) { return self.all.filter((document) => document.type === type); }, @@ -154,6 +153,10 @@ export const DocumentsModel = types }); }, + get exemplarDocuments() { + return self.byType(ExemplarDocument); + }, + get visibleExemplarDocuments() { return self.byType(ExemplarDocument).filter(e => self.isExemplarVisible(e.key)); }, diff --git a/src/models/stores/persistent-ui.ts b/src/models/stores/persistent-ui.ts index ccef297c1f..1969e4c4fd 100644 --- a/src/models/stores/persistent-ui.ts +++ b/src/models/stores/persistent-ui.ts @@ -2,11 +2,11 @@ import { getSnapshot, applySnapshot, types, onSnapshot } from "mobx-state-tree"; import { AppConfigModelType } from "./app-config-model"; -import { kDividerHalf, kDividerMax, kDividerMin } from "./ui-types"; +import { DocFilterType, DocFilterTypeEnum, kDividerHalf, kDividerMax, kDividerMin } from "./ui-types"; import { isWorkspaceModelSnapshot, WorkspaceModel } from "./workspace"; import { DocumentModelType } from "../document/document"; import { ENavTab } from "../view/nav-tabs"; -import { buildSectionPath, getCurriculumMetadata } from "../../../functions/src/shared"; +import { buildSectionPath, getCurriculumMetadata } from "../../../shared/shared"; import { ExemplarDocument, LearningLogDocument, LearningLogPublication, PersonalDocument, PersonalPublication, PlanningDocument, ProblemDocument, ProblemPublication, SupportPublication } from "../document/document-types"; @@ -35,6 +35,7 @@ export const PersistentUIModel = types .model("PersistentUI", { dividerPosition: kDividerHalf, activeNavTab: types.maybe(types.string), + docFilter: types.optional(DocFilterTypeEnum, "Problem"), showAnnotations: true, showTeacherContent: true, showChatPanel: false, @@ -187,6 +188,9 @@ export const PersistentUIModel = types }, setProblemPath(problemPath: string) { self.problemPath = problemPath; + }, + setDocFilter(docFilter: DocFilterType) { + self.docFilter = docFilter; } }; }) diff --git a/src/models/stores/section-docs-store.ts b/src/models/stores/section-docs-store.ts new file mode 100644 index 0000000000..6870ddf323 --- /dev/null +++ b/src/models/stores/section-docs-store.ts @@ -0,0 +1,100 @@ +import { makeAutoObservable } from "mobx"; +import { DocumentModelType } from "../document/document"; +import { isUnpublishedType, isPublishedType } from "../document/document-types"; +import { NavTabSectionModelType, ENavTabOrder } from "../view/nav-tabs"; +import { Bookmarks } from "./bookmarks"; +import { ClassModelType } from "./class"; +import { DocumentsModelType } from "./documents"; +import { UserModelType } from "./user"; + +export interface ISectionDocumentsStores { + bookmarks: Bookmarks; + class: ClassModelType; + documents: DocumentsModelType; + user: UserModelType; +} + +interface IMatchPropertiesOptions { + isTeacherDocument?: boolean; +} + +export class SectionDocuments { + stores: ISectionDocumentsStores; + + constructor(stores: ISectionDocumentsStores) { + makeAutoObservable(this); + this.stores = stores; + } + + + matchProperties(doc: DocumentModelType, properties?: readonly string[], options?: IMatchPropertiesOptions) { + // if no properties specified then consider it a match + if (!properties?.length) return true; + return properties?.every(p => { + const match = /(!)?(.*)/.exec(p); + const property = match && match[2]; + const wantsProperty = !(match && match[1]); // not negated => has property + // treat "starred" as a virtual property + // This will be a problem if we extract starred + if (property === "starred") { + return this.stores.bookmarks.isDocumentBookmarked(doc.key) === wantsProperty; + } + if (property === "isTeacherDocument") { + return !!options?.isTeacherDocument === wantsProperty; + } + if (property) { + return !!doc.getProperty(property) === wantsProperty; + } + // ignore empty strings, etc. + return true; + }); + } + + isMatchingSpec(doc: DocumentModelType, type: string, properties?: readonly string[]) { + return (type === doc.type) && this.matchProperties(doc, properties); + } + + isTeacherDocument(doc: DocumentModelType){ + return this.stores.class.isTeacher(doc.uid); + } + + getSectionDocs(section: NavTabSectionModelType): DocumentModelType[] { + let sectDocs: DocumentModelType[] = []; + (section.documentTypes || []).forEach(type => { + if (isUnpublishedType(type)) { + sectDocs.push(...this.stores.documents.byTypeForUser(type as any, this.stores.user.id)); + } + else if (isPublishedType(type)) { + const publishedDocs: { [source: string]: DocumentModelType[] } = {}; + this.stores.documents + .byType(type as any) + .forEach(doc => { + // personal documents and learning logs have originDocs. + // problem documents only have the uids of their creator, + // but as long as we're scoped to a single problem, there + // shouldn't be published documents from other problems. + const source = doc.originDoc || doc.uid; + if (source) { + if (!publishedDocs.source) { + publishedDocs.source = []; + } + publishedDocs.source.push(doc); + } + }); + for (const sourceId in publishedDocs) { + sectDocs.push(...publishedDocs[sourceId]); + } + } + }); + // Reverse the order to approximate a most-recently-used ordering. + if (section.order === ENavTabOrder.kReverse) { + sectDocs = sectDocs.reverse(); + } + // filter by additional properties + if (section.properties && section.properties.length) { + sectDocs = sectDocs.filter(doc => this.matchProperties(doc, section.properties, + { isTeacherDocument: this.isTeacherDocument(doc) })); + } + return sectDocs; + } +} diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index d1240be38e..43c1b03844 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -1,3 +1,4 @@ +import { IObservableArray, observable } from "mobx"; import { DocumentModelType, createDocumentModel, DocumentModelSnapshotType } from "../document/document"; import { GroupModel, GroupsModel, GroupsModelType, GroupUserModel } from './groups'; import { ClassModel, ClassModelType, ClassUserModel } from './class'; @@ -5,6 +6,7 @@ import { ProblemDocument } from '../document/document-types'; import { ISortedDocumentsStores, SortedDocuments } from "./sorted-documents"; import { DeepPartial } from "utility-types"; import { DocumentContentSnapshotType } from "../document/document-content"; +import { IDocumentMetadata } from "../../../shared/shared"; import "../tiles/text/text-registration"; import "../../plugins/drawing/drawing-registration"; @@ -31,6 +33,29 @@ const mockDocumentsData: DocumentModelSnapshotType[] = [ } ]; +const mockMetadataDocuments: IObservableArray = observable.array([ + { + uid: "1", //Joe + type: ProblemDocument, key:"Student 1 Problem Doc Group 5", createdAt: 1, + tools: [] + }, + { + uid: "2", //Scott + type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, + tools: ["Text"] + }, + { + uid: "3", //Dennis + type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, + tools: ["Drawing"] + }, + { + uid: "4", //Kirk + type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, + tools: [] + } +]); + const createMockDocuments = () => { return mockDocumentsData.map(createDocumentModel); }; @@ -134,29 +159,30 @@ describe('Sorted Documents Model', () => { }; sortedDocuments = new SortedDocuments(mockStores as ISortedDocumentsStores); + sortedDocuments.firestoreMetadataDocs = mockMetadataDocuments; }); - describe('sortByGroup Function', () => { + describe('byGroup Function', () => { it('should correctly sort documents by group', () => { - const sortedDocsByGroup = sortedDocuments.sortByGroup; + const sortedDocsByGroup = sortedDocuments.byGroup; expect(sortedDocsByGroup.length).toBe(3); - const group3 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 3'); + const group3 = sortedDocsByGroup.find(group => group.label === 'Group 3'); expect(group3?.documents.length).toBe(2); // Group 3 - Kirk + Scott - const group5 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 5'); + const group5 = sortedDocsByGroup.find(group => group.label === 'Group 5'); expect(group5?.documents.length).toBe(1); // Group 5 - Joe - const group9 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 9'); + const group9 = sortedDocsByGroup.find(group => group.label === 'Group 9'); expect(group9?.documents.length).toBe(1); // Group 9 - Dennis }); it('should sort the groups numerically from least to greatest', () => { //Verify "Group 3" comes before "Group 5" and before "Group 9" - const sortedSectionLabels = sortedDocuments.sortByGroup.map(group => group.sectionLabel); + const sortedSectionLabels = sortedDocuments.byGroup.map(group => group.label); expect(sortedSectionLabels).toEqual(['Group 3', 'Group 5', 'Group 9']); }); }); - describe('sortByName Function', () => { + describe('byName Function', () => { it('should correctly sort documents by last name', () => { const expectedOrder = [ "Bacal, Joe", @@ -164,17 +190,17 @@ describe('Sorted Documents Model', () => { "Cytacki, Scott", "Swenson, Kirk" ]; - const sortedDocsByName = sortedDocuments.sortByName; - const actualOrder = sortedDocsByName.map(group => group.sectionLabel); + const sortedDocsByName = sortedDocuments.byName; + const actualOrder = sortedDocsByName.map(group => group.label); expect(actualOrder).toEqual(expectedOrder); }); }); - describe('sortByTools Function', () => { + describe('byTools Function', () => { it('should correctly sort documents by tool', () => { - const sortedDocsByTools = sortedDocuments.sortByTools; + const sortedDocsByTools = sortedDocuments.byTools; const summaryOfResult = sortedDocsByTools.map(section => ({ - sectionLabel: section.sectionLabel, + sectionLabel: section.label, docKeys: section.documents.map(doc => doc.key) })); expect(summaryOfResult).toEqual([ diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index f80a62d3b3..1318b1b8d0 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -1,26 +1,31 @@ -import { ObservableSet, makeAutoObservable, runInAction } from "mobx"; -import { DocumentModelType } from "../document/document"; -import { isPublishedType, isSortableType, isUnpublishedType } from "../document/document-types"; +import { makeAutoObservable, runInAction, IObservableArray, observable } from "mobx"; +import { isSortableType } from "../document/document-types"; import { DocumentsModelType } from "./documents"; import { GroupsModelType } from "./groups"; import { ClassModelType } from "./class"; import { DB } from "../../lib/db"; import { AppConfigModelType } from "./app-config-model"; import { Bookmarks } from "./bookmarks"; -import { ENavTabOrder, NavTabSectionModelType } from "../view/nav-tabs"; import { UserModelType } from "./user"; +import { IDocumentMetadata } from "../../../shared/shared"; +import { typeConverter } from "../../utilities/db-utils"; +import { + createDocMapByBookmarks, + createDocMapByGroups, + createDocMapByNames, + createTileTypeToDocumentsMap, + getTagsWithDocs, + sortGroupSectionLabels, + sortNameSectionLabels +} from "../../utilities/sort-document-utils"; +import { DocumentGroup } from "./document-group"; import { getTileContentInfo } from "../tiles/tile-content-info"; -import { getTileComponentInfo } from "../tiles/tile-component-info"; +import { PrimarySortType } from "./ui-types"; +import { IArrowAnnotation } from "../annotations/arrow-annotation"; -import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; +export type SortedDocumentsMap = Record; -export type SortedDocument = { - sectionLabel: string; - documents: DocumentModelType[]; - icon?: React.FC>; //exists only in the "sort by tools" case -} - -type TagWithDocs = { +export type TagWithDocs = { tagKey: string; tagValue: string; docKeysFoundWithTag: string[]; @@ -36,351 +41,217 @@ export interface ISortedDocumentsStores { user: UserModelType; } -interface IMatchPropertiesOptions { - isTeacherDocument?: boolean; -} export class SortedDocuments { stores: ISortedDocumentsStores; firestoreTagDocumentMap = new Map>(); + firestoreMetadataDocs: IObservableArray = observable.array([]); constructor(stores: ISortedDocumentsStores) { makeAutoObservable(this); this.stores = stores; } - //********************************************* Views ******************************************* - get documents(): DocumentsModelType { - return this.stores.documents; - } - get groups(): GroupsModelType { - return this.stores.groups; + get bookmarksStore() { + return this.stores.bookmarks; } get class(): ClassModelType { return this.stores.class; } + get commentTags(): Record | undefined { + return this.stores.appConfig.commentTags; + } get db(): DB { return this.stores.db; } - get commentTags(): Record | undefined { - return this.stores.appConfig.commentTags; + get documents(): DocumentsModelType { + return this.stores.documents; } - get bookmarks() { - return this.stores.bookmarks; + get filteredDocsByType(): IDocumentMetadata[] { + return this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => { + return isSortableType(doc.type); + }); + } + get groupsStore(): GroupsModelType { + return this.stores.groups; } get user() { return this.stores.user; } - get filteredDocsByType(): DocumentModelType[] { - return this.documents.all.filter((doc: DocumentModelType) => { - return isSortableType(doc.type); - }); + sortBy(sortType: PrimarySortType): DocumentGroup[] { + switch (sortType) { + case "Group": + return this.byGroup; + case "Name": + return this.byName; + case "Strategy": + return this.byStrategy; + case "Tools": + return this.byTools; + case "Bookmarked": + return this.byBookmarked; + default: + return []; + } } - //******************************************* Sort By Group ************************************* - get sortByGroup(): SortedDocument[]{ - const documentMap = new Map(); - this.filteredDocsByType.forEach((doc) => { - const userId = doc.uid; - const group = this.groups.groupForUser(userId); - const sectionLabel = group ? `Group ${group.id}` : "No Group"; - if (!documentMap.has(sectionLabel)) { - documentMap.set(sectionLabel, { - sectionLabel, - documents: [] - }); - } - documentMap.get(sectionLabel).documents.push(doc); - }); - //sort from least to greatest - const sortedSectionLabels = Array.from(documentMap.keys()).sort((a, b) => { - const numA = parseInt(a.replace(/^\D+/g, ''), 10); - const numB = parseInt(b.replace(/^\D+/g, ''), 10); - return numA - numB; + // ** views ** // + get byGroup(): DocumentGroup[] { + const documentMap = createDocMapByGroups(this.filteredDocsByType, this.groupsStore.groupForUser); + const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map(label => { + return new DocumentGroup({stores: this.stores, label, documents: documentMap.get(label) ?? [] }); }); - return sortedSectionLabels.map(sectionLabel => documentMap.get(sectionLabel)); } - - //******************************************* Sort By Name ************************************** - get sortByName(): SortedDocument[]{ - const documentMap = new Map(); - this.filteredDocsByType.forEach((doc) => { - const user = this.class.getUserById(doc.uid); - const sectionLabel = user && `${user.lastName}, ${user.firstName}`; - if (!documentMap.has(sectionLabel)) { - documentMap.set(sectionLabel, { - sectionLabel, - documents: [] - }); - } - documentMap.get(sectionLabel).documents.push(doc); + get byName(): DocumentGroup[] { + const documentMap = createDocMapByNames(this.filteredDocsByType, this.class.getUserById); + const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map(label => { + return new DocumentGroup({ stores: this.stores, label, documents: documentMap.get(label) ?? [] }); }); - - const sortedSectionLabels = Array.from(documentMap.keys()).sort((a, b) => { - const parseName = (name: any) => { - const [lastName, firstName] = name.split(", ").map((part: any) => part.trim()); - return { firstName, lastName }; - }; - const aParsed = parseName(a); - const bParsed = parseName(b); - - // Compare by last name, then by first name if last names are equal - const lastNameCompare = aParsed.lastName.localeCompare(bParsed.lastName); - if (lastNameCompare !== 0) { - return lastNameCompare; - } - return aParsed.firstName.localeCompare(bParsed.firstName); - }); - return sortedSectionLabels.map(sectionLabel => documentMap.get(sectionLabel)); } - //*************************************** Sort By Strategy ************************************** - - get sortByStrategy(): SortedDocument[]{ + get byStrategy(): DocumentGroup[] { const commentTags = this.commentTags; - const tagsWithDocs: Record = {}; - if (commentTags) { - for (const key of Object.keys(commentTags)) { - tagsWithDocs[key] = { - tagKey: key, - tagValue: commentTags[key], - docKeysFoundWithTag: [] - }; - } - tagsWithDocs[""] = { //this accounts for when user commented with tagPrompt (no tag selected) - tagKey: "", - tagValue: "Not Tagged", - docKeysFoundWithTag: [] - }; - } - - // Find all unique document keys in tagsWithDocs. Compare this with all sortable documents - // in store to find "Documents with no comments" then place those doc keys to "Not Tagged" - const uniqueDocKeysWithTags = new Set(); - - // grouping documents based on firestore comment tags - this.firestoreTagDocumentMap.forEach((docKeysSet, tag) => { - const docKeysArray = Array.from(docKeysSet); // Convert the Set to an array - if (tagsWithDocs[tag]) { - docKeysSet.forEach((docKey: string) =>{ - uniqueDocKeysWithTags.add(docKey); - }); - tagsWithDocs[tag].docKeysFoundWithTag = docKeysArray; - } - }); - - // adding in (exemplar) documents with authored tags - const allSortableDocKeys = this.filteredDocsByType; - allSortableDocKeys.forEach(doc => { - const foundTagKey = doc.getProperty("authoredCommentTag"); - if (foundTagKey !== undefined && foundTagKey !== "") { - if (tagsWithDocs[foundTagKey]) { - tagsWithDocs[foundTagKey].docKeysFoundWithTag.push(doc.key); - uniqueDocKeysWithTags.add(doc.key); - } - } - }); - - allSortableDocKeys.forEach(doc => { - if (!uniqueDocKeysWithTags.has(doc.key)) { - // This document has no comments - if (tagsWithDocs[""]) { - tagsWithDocs[""].docKeysFoundWithTag.push(doc.key); - } - } - }); + const tagsWithDocs = getTagsWithDocs(this.firestoreMetadataDocs, commentTags, this.firestoreTagDocumentMap); - const sortedDocsArr: SortedDocument[] = []; + const sortedDocsArr: DocumentGroup[] = []; Object.entries(tagsWithDocs).forEach((tagKeyAndValObj) => { const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; - const sectionLabel = tagWithDocs.tagValue; + const label = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; - const documents = this.documents.all.filter(doc => docKeys.includes(doc.key)); - sortedDocsArr.push({ - sectionLabel, - documents - }); + const documents = this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); + sortedDocsArr.push(new DocumentGroup({ stores: this.stores, label, documents })); }); return sortedDocsArr; } - async updateTagDocumentMap () { - const db = this.db.firestore; - const filteredDocs = this.filteredDocsByType; - filteredDocs.forEach(async doc => { - const docsSnapshot = await db.collection("documents").where("key", "==", doc.key) - .where("context_id", "==", this.user.classHash).get(); - docsSnapshot.docs.forEach(async docSnapshot => { - const commentsSnapshot = await docSnapshot.ref.collection("comments").get(); - runInAction(() => { - commentsSnapshot.docs.forEach(commentDoc => { - const commentData = commentDoc.data(); - if (commentData?.tags) { - commentData.tags.forEach((tag: string) => { - let docKeysSet = this.firestoreTagDocumentMap.get(tag); - if (!docKeysSet) { - docKeysSet = new ObservableSet(); - this.firestoreTagDocumentMap.set(tag, docKeysSet); - } - docKeysSet.add(doc.key); - }); - } - }); - }); - }); - }); - } - - //*************************************** Sort By Bookmarks ************************************* - - get sortByBookmarks(): SortedDocument[] { - const documentMap = new Map(); - this.filteredDocsByType.forEach((doc) => { - const sectionLabel = this.bookmarks.isDocumentBookmarked(doc.key) ? "Bookmarked" : "Not Bookmarked"; - if (!documentMap.has(sectionLabel)) { - documentMap.set(sectionLabel, { - sectionLabel, - documents: [] - }); - } - documentMap.get(sectionLabel).documents.push(doc); - }); - - const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; - return sortedSectionLabels.filter(label => documentMap.has(label)).map(label => documentMap.get(label)); - } - - //**************************************** Sort By Tools **************************************** + get byTools(): DocumentGroup[] { + const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.firestoreMetadataDocs); - get sortByTools(): SortedDocument[] { - const tileTypeToDocumentsMap: Record = {}; + const sectionedDocuments = Array.from(tileTypeToDocumentsMap.keys()).map(tileType => { - const addDocByType = (docToAdd: DocumentModelType, type: string) => { - if (!tileTypeToDocumentsMap[type]) { - tileTypeToDocumentsMap[type] = []; - } - tileTypeToDocumentsMap[type].push(docToAdd); - }; - - //Iterate through all documents, determine if they are valid, - //create a map of valid ones, otherwise put them into the "No Tools" section - this.filteredDocsByType.forEach((doc) => { - const tilesByTypeMap = doc.content?.getAllTilesByType(); - if (tilesByTypeMap) { - const tileTypes = Object.keys(tilesByTypeMap); - const validTileTypes = tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); - if (validTileTypes.length > 0) { - validTileTypes.forEach(tileType => { - addDocByType(doc, tileType); - }); - - //Assuming validTileTypes, we can check if the document has "Sparrow" annotations - const docHasAnnotations = doc.content?.annotations && doc.content?.annotations.size > 0; - if(docHasAnnotations){ - addDocByType(doc, "Sparrow"); - } - } else { //Documents with only all Placeholder or Unknown tiles - addDocByType(doc, "No Tools"); - } - } - }); - - // Map the tile types to their display names - const sectionedDocuments = Object.keys(tileTypeToDocumentsMap).map(tileType => { - const section: SortedDocument = { - sectionLabel: tileType, - documents: tileTypeToDocumentsMap[tileType], - }; - if (tileType === "Sparrow") { - section.icon = SparrowHeaderIcon; - } else { - const contentInfo = getTileContentInfo(tileType); - section.sectionLabel = contentInfo?.displayName || tileType; - const componentInfo = getTileComponentInfo(tileType); - section.icon = componentInfo?.HeaderIcon; - } + const contentInfo = getTileContentInfo(tileType); + const label = contentInfo?.displayName || tileType; + const documents = tileTypeToDocumentsMap.get(tileType)?.documents ?? []; + const icon = tileTypeToDocumentsMap.get(tileType)?.icon; + const section = new DocumentGroup({ stores: this.stores, label, documents, icon }); return section; }); // Sort the tile types. 'No Tools' should be at the end. const sortedByLabel = sectionedDocuments.sort((a, b) => { - if (a.sectionLabel === "No Tools") return 1; // Move 'No Tools' to the end - if (b.sectionLabel === "No Tools") return -1; // Alphabetically sort all others - return a.sectionLabel.localeCompare(b.sectionLabel); + if (a.label === "No Tools") return 1; // Move 'No Tools' to the end + if (b.label === "No Tools") return -1; // Alphabetically sort all others + return a.label.localeCompare(b.label); }); return sortedByLabel; } - matchProperties(doc: DocumentModelType, properties?: readonly string[], options?: IMatchPropertiesOptions) { - // if no properties specified then consider it a match - if (!properties?.length) return true; - return properties?.every(p => { - const match = /(!)?(.*)/.exec(p); - const property = match && match[2]; - const wantsProperty = !(match && match[1]); // not negated => has property - // treat "starred" as a virtual property - // This will be a problem if we extract starred - if (property === "starred") { - return this.bookmarks.isDocumentBookmarked(doc.key) === wantsProperty; - } - if (property === "isTeacherDocument") { - return !!options?.isTeacherDocument === wantsProperty; - } - if (property) { - return !!doc.getProperty(property) === wantsProperty; - } - // ignore empty strings, etc. - return true; - }); - } + get byBookmarked(): DocumentGroup[] { + const documentMap = createDocMapByBookmarks(this.firestoreMetadataDocs, this.bookmarksStore); - isMatchingSpec(doc: DocumentModelType, type: string, properties?: readonly string[]) { - return (type === doc.type) && this.matchProperties(doc, properties); + const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; + return sortedSectionLabels.filter(label => documentMap.has(label)) + .map(label => new DocumentGroup({ + stores: this.stores, + label, + documents: documentMap.get(label) ?? [] + })); } - isTeacherDocument(doc: DocumentModelType){ - return this.class.isTeacher(doc.uid); - } + async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { + const db = this.db.firestore; + const converter = typeConverter(); + let query = db.collection("documents").withConverter(converter).where("context_id", "==", this.user.classHash); - getSectionDocs(section: NavTabSectionModelType): DocumentModelType[] { - let sectDocs: DocumentModelType[] = []; - (section.documentTypes || []).forEach(type => { - if (isUnpublishedType(type)) { - sectDocs.push(...this.documents.byTypeForUser(type as any, this.user.id)); - } - else if (isPublishedType(type)) { - const publishedDocs: { [source: string]: DocumentModelType[] } = {}; - this.documents - .byType(type as any) - .forEach(doc => { - // personal documents and learning logs have originDocs. - // problem documents only have the uids of their creator, - // but as long as we're scoped to a single problem, there - // shouldn't be published documents from other problems. - const source = doc.originDoc || doc.uid; - if (source) { - if (!publishedDocs.source) { - publishedDocs.source = []; - } - publishedDocs.source.push(doc); - } - }); - for (const sourceId in publishedDocs) { - sectDocs.push(...publishedDocs[sourceId]); - } - } - }); - // Reverse the order to approximate a most-recently-used ordering. - if (section.order === ENavTabOrder.kReverse) { - sectDocs = sectDocs.reverse(); + if (filter !== "All") { + query = query.where("unit" , "==", unit); } - // filter by additional properties - if (section.properties && section.properties.length) { - sectDocs = sectDocs.filter(doc => this.matchProperties(doc, section.properties, - { isTeacherDocument: this.isTeacherDocument(doc) })); + if (filter === "Investigation" || filter === "Problem") { + query = query.where("investigation", "==", String(investigation)); } - return sectDocs; + if (filter === "Problem") { + query = query.where("problem", "==", String(problem)); + } + const queryForUnitNull = db.collection("documents").withConverter(converter) + .where("context_id", "==", this.user.classHash) + .where("unit" , "==", null); + const [docsWithUnit, docsWithoutUnit] = await Promise.all([query.get(), queryForUnitNull.get()]); + const docsArray: IDocumentMetadata[] = []; + + const matchedDocKeys = new Set(); + docsWithUnit.docs.forEach(doc => { + if (matchedDocKeys.has(doc.data().key)) return; + docsArray.push(doc.data()); + matchedDocKeys.add(doc.data().key); + }); + docsWithoutUnit.docs.forEach(doc => { + if (matchedDocKeys.has(doc.data().key)) return; + docsArray.push(doc.data()); + matchedDocKeys.add(doc.data().key); + }); + + // Add Exemplar documents, which should have been loaded into the documents + // store but are not found in the firestore query -- they are authored as + // content, not found in the database. + this.stores.documents.exemplarDocuments.forEach(doc => { + const exemplarStrategy = doc.properties.get('authoredCommentTag'); + + const tools: string[] = []; + const contentTileTypes = doc.content?.tileTypes || []; + const annotationsArray = Array.from(doc.content?.annotations || []); + const annotationTypes = annotationsArray.map(([key, annotation]: [string, IArrowAnnotation]) => annotation.type); + contentTileTypes.forEach(tileType => tools.push(tileType)); + if (annotationTypes.includes("arrowAnnotation")) { + tools.push("Sparrow"); + } + + const metadata: IDocumentMetadata = { + uid: doc.uid, + type: doc.type, + key: doc.key, + createdAt: doc.createdAt, + title: doc.title, + properties: undefined, + tools, + strategies: exemplarStrategy ? [exemplarStrategy] : [], + investigation: doc.investigation, + problem: doc.problem, + unit: doc.unit + }; + docsArray.push(metadata); + }); + + runInAction(() => { + this.firestoreMetadataDocs.replace(docsArray); + }); } + async fetchFullDocument(docKey: string) { + const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); + if (!metadataDoc) return; + + const unit = metadataDoc?.unit ?? undefined; + const visibility = metadataDoc?.visibility === "public" || metadataDoc?.visibility === "private" + ? metadataDoc?.visibility as "public" | "private" + : undefined; + const props = { + documentKey: metadataDoc?.key, + type: metadataDoc?.type as any, + title: metadataDoc?.title || undefined, + properties: metadataDoc?.properties, + userId: metadataDoc?.uid, + groupId: undefined, + visibility, + originDoc: undefined, + pubVersion: undefined, + problem: metadataDoc?.problem, + investigation: metadataDoc?.investigation, + unit, + }; + + return this.db.openDocument(props); + } } diff --git a/src/models/stores/stores.ts b/src/models/stores/stores.ts index f371cc62e8..75c464eb30 100644 --- a/src/models/stores/stores.ts +++ b/src/models/stores/stores.ts @@ -34,6 +34,7 @@ import { createAndLoadExemplarDocs } from "./create-exemplar-docs"; import curriculumConfigJson from "../../clue/curriculum-config.json"; import { gImageMap } from "../image-map"; import { ExemplarControllerModel, ExemplarControllerModelType } from "./exemplar-controller"; +import { SectionDocuments } from "./section-docs-store"; export interface IStores extends IBaseStores { problemPath: string; @@ -46,6 +47,7 @@ export interface IStores extends IBaseStores { initializeStudentWorkTab: () => void; loadUnitAndProblem: (unitId: string | undefined, problemOrdinal?: string) => Promise; sortedDocuments: SortedDocuments; + sectionDocuments: SectionDocuments; unitLoadedPromise: Promise; sectionsLoadedPromise: Promise; startedLoadingUnitAndProblem: boolean; @@ -90,6 +92,7 @@ class Stores implements IStores{ serialDevice: SerialDevice; userContextProvider: UserContextProvider; sortedDocuments: SortedDocuments; + sectionDocuments: SectionDocuments; unitLoadedPromise: Promise; sectionsLoadedPromise: Promise; startedLoadingUnitAndProblem: boolean; @@ -150,6 +153,7 @@ class Stores implements IStores{ this.userContextProvider = new UserContextProvider(this); this.bookmarks = new Bookmarks({db: this.db}); this.sortedDocuments = new SortedDocuments(this); + this.sectionDocuments = new SectionDocuments(this); this.unitLoadedPromise = when(() => this.unit !== defaultUnit); this.sectionsLoadedPromise = when(() => this.problem.sections.length > 0); @@ -265,7 +269,9 @@ class Stores implements IStores{ }); showLoadingMessage("Loading exemplar documents"); createAndLoadExemplarDocs({ + unit, unitUrl: unitUrls.content, + investigation, problem, documents: this.documents, user: this.user, diff --git a/src/models/stores/ui-types.ts b/src/models/stores/ui-types.ts index 606c1d0ad8..cb2f9e2fb5 100644 --- a/src/models/stores/ui-types.ts +++ b/src/models/stores/ui-types.ts @@ -2,6 +2,10 @@ import { Instance, types } from "mobx-state-tree"; export const UIDialogTypeEnum = types.enumeration("dialogType", ["alert", "confirm", "prompt"]); export type UIDialogType = Instance; +export const DocFilterTypeEnum = types.enumeration("docFilter", ["Problem", "Investigation", "Unit", "All"]); +export type DocFilterType = Instance; +export type PrimarySortType = "Bookmarked" | "Group" | "Name" | "Strategy" | "Tools"; +export type SecondarySortType = PrimarySortType | "None"; export const kDividerMin = 0; // left side (resources/navigation) is collapsed export const kDividerHalf = 50; // resources/navigation and workspace are split 50/50 diff --git a/src/models/tiles/log/log-comment-event.ts b/src/models/tiles/log/log-comment-event.ts index 3ce4b4c5b8..dd6b907940 100644 --- a/src/models/tiles/log/log-comment-event.ts +++ b/src/models/tiles/log/log-comment-event.ts @@ -1,4 +1,4 @@ -import { isSectionPath, parseSectionPath } from "../../../../functions/src/shared"; +import { isSectionPath, parseSectionPath } from "../../../../shared/shared"; import { ProblemModelType } from "../../curriculum/problem"; import { Logger } from "../../../lib/logger"; import { getTileTitleForLogging } from "../../../lib/logger-utils"; diff --git a/src/plugins/bar-graph/assets/bar-graph-icon.svg b/src/plugins/bar-graph/assets/bar-graph-icon.svg new file mode 100644 index 0000000000..c0c315d139 --- /dev/null +++ b/src/plugins/bar-graph/assets/bar-graph-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/plugins/bar-graph/bar-graph-chart.tsx b/src/plugins/bar-graph/bar-graph-chart.tsx new file mode 100644 index 0000000000..42e15c71bd --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-chart.tsx @@ -0,0 +1,214 @@ +import { AxisBottom, AxisLeft } from "@visx/axis"; +import { GridRows } from "@visx/grid"; +import { Group } from "@visx/group"; +import { scaleBand, scaleLinear } from "@visx/scale"; +import { Bar, BarGroup } from "@visx/shape"; +import { isNumber } from "lodash"; +import { observer } from "mobx-react"; +import React, { useMemo } from "react"; +import { clueDataColorInfo } from "../../utilities/color-utils"; +import { useBarGraphModelContext } from "./bar-graph-content-context"; +import { CategoryPulldown } from "./category-pulldown"; +import EditableAxisLabel from "./editable-axis-label"; + +const margin = { + top: 60, + bottom: 60, + left: 80, + right: 80, +}; + +const demoCases: Record[] = [ + { date: '6/23/24', location: 'deck' }, + { date: '6/23/24', location: 'porch' }, + { date: '6/23/24', location: 'tree' }, + { date: '6/24/24', location: 'porch' }, + { date: '6/24/24', location: 'porch' }, + { date: '6/25/24', location: 'backyard' }, + { date: '6/25/24', location: 'deck' }, + { date: '6/25/24', location: 'deck' }, + { date: '6/25/24', location: 'deck' }, + { date: '6/25/24', location: 'tree' }, + { date: '6/26/24', location: 'backyard' }, + { date: '6/26/24', location: 'deck' }, + { date: '6/26/24', location: 'deck' }, + { date: '6/26/24', location: 'porch' }, + { date: '6/26/24', location: 'tree' } +]; + +function roundTo5(n: number): number { + return Math.ceil(n/5)*5; +} + +function barColor(n: number) { + return clueDataColorInfo[n % clueDataColorInfo.length].color; +} + +interface IBarGraphChartProps { + width: number; + height: number; +} + +// TODO rotate labels if needed +// angle: -45, textAnchor: 'end' +// https://github.com/airbnb/visx/discussions/1494 + + +export const BarGraphChart = observer(function BarGraphChart({ width, height }: IBarGraphChartProps) { + + const model = useBarGraphModelContext(); + const primary = model?.primaryAttribute || "date"; + const secondary = model?.secondaryAttribute || "location"; + + const xMax = width - margin.left - margin.right; + const yMax = height - margin.top - margin.bottom; + + function setDemoCategory(catname: string) { + if (catname === "date") { + model?.setPrimaryAttribute("date"); + model?.setSecondaryAttribute("location"); + } else{ + model?.setPrimaryAttribute("location"); + model?.setSecondaryAttribute("date"); + } + } + + // Count cases and make the data array + const data = useMemo( + () => demoCases.reduce((acc, row) => { + const cat = primary in row ? row[primary] : ""; + const subCat = row[secondary] || ""; + const index = acc.findIndex(r => r[primary] === cat); + if (index >= 0) { + const cur = acc[index][subCat]; + acc[index][subCat] = (isNumber(cur) ? cur : 0) + 1; + } else { + const newRow = { [primary]: cat, [subCat]: 1 }; + acc.push(newRow); + } + return acc; + }, [] as { [key: string]: number|string }[]), + [primary, secondary]); + + const primaryKeys: string[] + = useMemo(() => data.map(d => d[primary] as string), + [data, primary]); + const secondaryKeys: string[] + = useMemo(() => Array.from(new Set(data.flatMap(d => Object.keys(d)).filter(k => k !== primary))), + [data, primary]); + + // find the maximum data value + const maxValue = data.reduce((acc, row) => { + const rowValues = Object.values(row).slice(1) as (string | number)[]; + const maxInRow = Math.max(...rowValues.map(v => isNumber(v) ? v : 0)); + return Math.max(maxInRow, acc); + }, 0); + + const primaryScale = useMemo( + () => + scaleBand({ + domain: primaryKeys, + padding: 0.2, + range: [0, xMax]}), + [xMax, primaryKeys]); + + const secondaryScale = useMemo( + () => + scaleBand({ + domain: secondaryKeys, + padding: 0.2, + range: [0, primaryScale.bandwidth()]}), + [primaryScale, secondaryKeys]); + + const countScale = useMemo( + () => + scaleLinear({ + domain: [0, roundTo5(maxValue)], + range: [yMax, 0], + }), + [yMax, maxValue]); + + if (xMax <= 0 || yMax <= 0) return Too small; + + const ticks = Math.min(4, Math.floor(yMax/40)); // leave generous vertical space (>=40 px) between ticks + const labelWidth = (xMax/primaryKeys.length)-10; // setting width will wrap lines in labels when needed + + return ( + + + + + Number(value).toFixed(0)} + /> + barColor(i)} + className="bar" + keys={secondaryKeys} + height={yMax} + x0={(d) => d[primary] as string} + x0Scale={primaryScale} + x1Scale={secondaryScale} + yScale={countScale} + > + {(barGroups) => + + {barGroups.map((barGroup) => ( + + {barGroup.bars.map((bar) => { + if(!bar.value) return null; + return ; + })} + + ))} + + } + + + model?.setYAxisLabel(text)} + /> + + + ); +}); diff --git a/src/plugins/bar-graph/bar-graph-content-context.ts b/src/plugins/bar-graph/bar-graph-content-context.ts new file mode 100644 index 0000000000..9d06da4dbb --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-content-context.ts @@ -0,0 +1,6 @@ +import { createContext, useContext } from "react"; +import { BarGraphContentModelType } from "./bar-graph-content"; + +export const BarGraphModelContext = createContext({} as BarGraphContentModelType); + +export const useBarGraphModelContext = () => useContext(BarGraphModelContext); diff --git a/src/plugins/bar-graph/bar-graph-content.test.ts b/src/plugins/bar-graph/bar-graph-content.test.ts new file mode 100644 index 0000000000..1b5a7029c4 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-content.test.ts @@ -0,0 +1,39 @@ +import { defaultBarGraphContent, BarGraphContentModel } from "./bar-graph-content"; + +describe("Bar Graph Content", () => { + it("is a Bar Graph model", () => { + const content = BarGraphContentModel.create(); + expect(content.type).toBe("BarGraph"); + }); + + it("yAxisLabel has default content of 'Counts'", () => { + const content = defaultBarGraphContent(); + expect(content.yAxisLabel).toBe("Counts"); + }); + + it("is always user resizable", () => { + const content = BarGraphContentModel.create(); + expect(content.isUserResizable).toBe(true); + }); + + it("supports changing the y axis label", () => { + const content = BarGraphContentModel.create(); + content.setYAxisLabel("New Text"); + expect(content.yAxisLabel).toBe("New Text"); + }); + + it("supports setting the primary attribute", () => { + const content = BarGraphContentModel.create(); + expect(content.primaryAttribute).toBeUndefined(); + content.setPrimaryAttribute("attrId"); + expect(content.primaryAttribute).toBe("attrId"); + }); + + it("supports setting the secondary attribute", () => { + const content = BarGraphContentModel.create(); + expect(content.secondaryAttribute).toBeUndefined(); + content.setSecondaryAttribute("attrId"); + expect(content.secondaryAttribute).toBe("attrId"); + }); + +}); diff --git a/src/plugins/bar-graph/bar-graph-content.ts b/src/plugins/bar-graph/bar-graph-content.ts new file mode 100644 index 0000000000..fbe79b2649 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-content.ts @@ -0,0 +1,39 @@ +import { types, Instance } from "mobx-state-tree"; +import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content"; +import { kBarGraphTileType, kBarGraphContentType } from "./bar-graph-types"; + +export function defaultBarGraphContent(): BarGraphContentModelType { + return BarGraphContentModel.create({yAxisLabel: "Counts"}); +} + +export const BarGraphContentModel = TileContentModel + .named(kBarGraphContentType) + .props({ + type: types.optional(types.literal(kBarGraphTileType), kBarGraphTileType), + yAxisLabel: "", + primaryAttribute: types.maybe(types.string), + secondaryAttribute: types.maybe(types.string) + }) + .views(self => ({ + get isUserResizable() { + return true; + } + })) + .actions(self => ({ + setYAxisLabel(text: string) { + self.yAxisLabel = text; + }, + setPrimaryAttribute(attrId: string) { + self.primaryAttribute = attrId; + }, + setSecondaryAttribute(attrId: string) { + self.secondaryAttribute = attrId; + } + })); + +export interface BarGraphContentModelType extends Instance {} + + +export function isBarGraphModel(model?: ITileContentModel): model is BarGraphContentModelType { + return model?.type === kBarGraphTileType; +} diff --git a/src/plugins/bar-graph/bar-graph-registration.ts b/src/plugins/bar-graph/bar-graph-registration.ts new file mode 100644 index 0000000000..3de8812d01 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-registration.ts @@ -0,0 +1,23 @@ +import { registerTileComponentInfo } from "../../models/tiles/tile-component-info"; +import { registerTileContentInfo } from "../../models/tiles/tile-content-info"; +import { kBarGraphTileType, kBarGraphDefaultHeight } from "./bar-graph-types"; +import { BarGraphComponent } from "./bar-graph-tile"; +import { defaultBarGraphContent, BarGraphContentModel } from "./bar-graph-content"; + +import Icon from "./assets/bar-graph-icon.svg"; + +registerTileContentInfo({ + type: kBarGraphTileType, + displayName: "Bar Graph", + modelClass: BarGraphContentModel, + defaultContent: defaultBarGraphContent, + defaultHeight: kBarGraphDefaultHeight +}); + +registerTileComponentInfo({ + type: kBarGraphTileType, + Component: BarGraphComponent, + tileEltClass: "bar-graph-tile", + Icon, + HeaderIcon: Icon // TODO do we need a separate header icon? +}); diff --git a/src/plugins/bar-graph/bar-graph-tile.test.tsx b/src/plugins/bar-graph/bar-graph-tile.test.tsx new file mode 100644 index 0000000000..df552f64dd --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-tile.test.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { render, getByText as globalGetByText } from "@testing-library/react"; + +import { ITileApi } from "../../components/tiles/tile-api"; +import { TileModel } from "../../models/tiles/tile-model"; +import { defaultBarGraphContent } from "./bar-graph-content"; +import { BarGraphComponent } from "./bar-graph-tile"; + +// The tile needs to be registered so the TileModel.create +// knows it is a supported tile type +import "./bar-graph-registration"; + +jest.mock("react-resize-detector", () => ({ + useResizeDetector: jest.fn(() => ({height: 200, width: 200, ref: null})) +})); + +jest.mock("./bar-graph-utils", () => ({ + getBBox: jest.fn(() => ({x: 0, y: 0, width: 500, height: 200})) +})); + + +describe("BarGraphComponent", () => { + const content = defaultBarGraphContent(); + const model = TileModel.create({content}); + + const defaultProps = { + tileElt: null, + context: "", + docId: "", + documentContent: null, + isUserResizable: true, + onResizeRow: (e: React.DragEvent): void => { + throw new Error("Function not implemented."); + }, + onSetCanAcceptDrop: (tileId?: string): void => { + throw new Error("Function not implemented."); + }, + onRequestRowHeight: (tileId: string, height?: number, deltaHeight?: number): void => { + throw new Error("Function not implemented."); + }, + onRegisterTileApi: (tileApi: ITileApi, facet?: string): void => { + throw new Error("Function not implemented."); + }, + onUnregisterTileApi: (facet?: string): void => { + throw new Error("Function not implemented."); + } + }; + + it("renders successfully", () => { + const {getByText, getByTestId} = + render(); + expect(getByText("Tile Title")).toBeInTheDocument(); + expect(getByTestId("bar-graph-content")).toBeInTheDocument(); + expect(globalGetByText(getByTestId("bar-graph-content"), "Counts")).toBeInTheDocument(); + expect(getByText("6/23/24")).toBeInTheDocument(); + }); + + it.skip("updates the text when the model changes", async () => { + const {getByTestId, findByText} = + render(); + expect(globalGetByText(getByTestId("bar-graph-content"), "Counts")).toBeInTheDocument(); + + content.setYAxisLabel("New Text"); + + expect(await findByText( "New Text")).toBeInTheDocument(); + }); + +}); diff --git a/src/plugins/bar-graph/bar-graph-tile.tsx b/src/plugins/bar-graph/bar-graph-tile.tsx new file mode 100644 index 0000000000..d307016faf --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-tile.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react"; +import { useResizeDetector } from "react-resize-detector"; + +import { BasicEditableTileTitle } from "../../components/tiles/basic-editable-tile-title"; +import { ITileProps } from "../../components/tiles/tile-component"; +import { BarGraphChart } from "./bar-graph-chart"; +import { BarGraphModelContext } from "./bar-graph-content-context"; +import { isBarGraphModel } from "./bar-graph-content"; + +import "./bar-graph.scss"; + +export const BarGraphComponent: React.FC = observer((props: ITileProps) => { + + const {height: resizeHeight, width: resizeWidth, ref} = useResizeDetector(); + + const { model, readOnly } = props; + + const content = isBarGraphModel(model.content) ? model.content : null; + + + return ( + + +
+ +
+
+ ); +}); + +BarGraphComponent.displayName = "BarGraphComponent"; diff --git a/src/plugins/bar-graph/bar-graph-types.ts b/src/plugins/bar-graph/bar-graph-types.ts new file mode 100644 index 0000000000..9bbbf9cc49 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-types.ts @@ -0,0 +1,5 @@ +export const kBarGraphTileType = "BarGraph"; + +export const kBarGraphContentType = "BarGraphContentModel"; + +export const kBarGraphDefaultHeight = 320; diff --git a/src/plugins/bar-graph/bar-graph-utils.ts b/src/plugins/bar-graph/bar-graph-utils.ts new file mode 100644 index 0000000000..991175e7b3 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-utils.ts @@ -0,0 +1,5 @@ + +// Just wraps the native getBBox method to make it mockable in tests +export function getBBox(element: SVGGraphicsElement): DOMRect { + return element.getBBox(); +} diff --git a/src/plugins/bar-graph/bar-graph.scss b/src/plugins/bar-graph/bar-graph.scss new file mode 100644 index 0000000000..20fc30859e --- /dev/null +++ b/src/plugins/bar-graph/bar-graph.scss @@ -0,0 +1,56 @@ +@import "../../components/vars.sass"; + +.bar-graph-content { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + overflow: auto; + + svg { + width: 100%; + height: 100%; + + .visx-bar-group .visx-bar { + stroke: black; + stroke-width: 1px; + } + + foreignObject { + + input { + position: absolute; + left: 0; + border: 2px solid blue; + padding: 3px; + } + + button { + font-family: Lato; + border: 1.5px solid #949494; + border-radius: 5px; + padding: 5px; + + .button-content { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + svg { + max-height: 1em; + max-width: 1em; + margin-left: 1em; + opacity: .35; + } + } + + } + } + + } + +} + diff --git a/src/plugins/bar-graph/category-pulldown.tsx b/src/plugins/bar-graph/category-pulldown.tsx new file mode 100644 index 0000000000..d3a78264c5 --- /dev/null +++ b/src/plugins/bar-graph/category-pulldown.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Menu, MenuButton, MenuItem, MenuList, Portal } from "@chakra-ui/react"; +import { useReadOnlyContext } from "../../components/document/read-only-context"; + +import DropdownCaretIcon from "../dataflow/assets/icons/dropdown-caret.svg"; + + +interface IProps { + categoryList: string[]; + category: string; + setCategory: (category: string) => void; + x: number; + y: number; + width: number; + height: number; +} + +export function CategoryPulldown({categoryList, category, setCategory, x, y, width, height}: IProps) { + const readOnly = useReadOnlyContext(); + + return ( + + + + + {category} + + + + + + {categoryList.map((c) => ( + setCategory(c)}>{c} + ))} + + + + + ); +} diff --git a/src/plugins/bar-graph/editable-axis-label.tsx b/src/plugins/bar-graph/editable-axis-label.tsx new file mode 100644 index 0000000000..5b625079d1 --- /dev/null +++ b/src/plugins/bar-graph/editable-axis-label.tsx @@ -0,0 +1,105 @@ +import React, { useEffect } from 'react'; +import { Text } from '@visx/text'; +import { getBBox } from './bar-graph-utils'; +import { useReadOnlyContext } from '../../components/document/read-only-context'; + +const paddingX = 5, paddingY = 10; + +interface Props { + x: number; + y: number; + text?: string; + setText: (text: string) => void; +} + +const EditableAxisLabel: React.FC = ({text, x, y, setText}) => { + + const readOnly = useReadOnlyContext(); + const textRef = React.useRef(null); + const [boundingBox, setBoundingBox] = React.useState(null); + const [editing, setEditing] = React.useState(false); + const [displayText, setDisplayText] = React.useState(text || "Y axis"); + const [editText, setEditText] = React.useState(text || "Y axis"); + + useEffect(() => { + if (textRef.current) { + const bb = getBBox(textRef.current); + setBoundingBox(bb); + } + }, [x, y, displayText, textRef]); + + const handleClose = (accept: boolean) => { + setEditing(false); + if (accept && editText) { + const trimmed = editText.trim(); + setDisplayText(trimmed); + setText(trimmed); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const { key } = e; + switch (key) { + case "Escape": + handleClose(false); + break; + case "Enter": + case "Tab": + handleClose(true); + break; + } + }; + + if (editing) { + return ( + + handleClose(true)} + onChange={(e) => setEditText(e.target.value)} + /> + + ); + } + + return ( + + {boundingBox && + { if (!readOnly) setEditing(true); }} + />} + + + {displayText} + + + + ); +}; + +export default EditableAxisLabel; diff --git a/src/public/demo/units/qa-moth-plot/exemplars/investigation-1/problem-1/exemplar-1/content.json b/src/public/demo/units/qa-moth-plot/exemplars/investigation-1/problem-1/exemplar-1/content.json index 6678afc796..f5593f5f0f 100644 --- a/src/public/demo/units/qa-moth-plot/exemplars/investigation-1/problem-1/exemplar-1/content.json +++ b/src/public/demo/units/qa-moth-plot/exemplars/investigation-1/problem-1/exemplar-1/content.json @@ -1,6 +1,6 @@ { "title": "First Exemplar", - "tag": "unit-rate", + "tag": "materials", "content": { "tiles": [ { diff --git a/src/public/demo/units/qa/content.json b/src/public/demo/units/qa/content.json index 0ebbf43e57..c547070217 100644 --- a/src/public/demo/units/qa/content.json +++ b/src/public/demo/units/qa/content.json @@ -81,6 +81,7 @@ {"id": "Starter", "title": "Starter", "isTileTool": true}, {"id": "Simulator", "title": "EMG Simulator", "isTileTool": true}, {"id": "Expression", "title": "Expression", "isTileTool": true}, + {"id": "BarGraph", "title": "Bar Graph", "isTileTool": true}, {"id": "undo", "title": "Undo", "iconId": "icon-undo-tool", "isTileTool": false}, {"id": "redo", "title": "Redo", "iconId": "icon-redo-tool", "isTileTool": false}, {"id": "duplicate", "title": "Duplicate", "iconId": "icon-duplicate-tool", "isTileTool": false}, diff --git a/src/register-tile-types.ts b/src/register-tile-types.ts index 606673764d..49db66fc14 100644 --- a/src/register-tile-types.ts +++ b/src/register-tile-types.ts @@ -14,6 +14,10 @@ function loggedLoad(name: string, imports: () => Promise[]) { const gTileRegistration: Record void> = { + "BarGraph": loggedLoad("BarGraph", () => [ + import(/* webpackChunkName: "BarGraph" */"./plugins/bar-graph/bar-graph-registration"), + import(/* webpackChunkName: "SharedDataSet" */"./models/shared/shared-data-set-registration") + ]), "DataCard": loggedLoad("DataCard", () => [ import(/* webpackChunkName: "DataCard" */"./plugins/data-card/data-card-registration"), import(/* webpackChunkName: "SharedDataSet" */"./models/shared/shared-data-set-registration") diff --git a/src/utilities/db-utils.ts b/src/utilities/db-utils.ts new file mode 100644 index 0000000000..88bcfafc78 --- /dev/null +++ b/src/utilities/db-utils.ts @@ -0,0 +1,9 @@ +import firebase from "firebase/app"; + +// https://medium.com/swlh/using-firestore-with-typescript-65bd2a602945 +export const typeConverter = (): + firebase.firestore.FirestoreDataConverter => +({ + toFirestore: (data: T) => data, + fromFirestore: (doc: firebase.firestore.QueryDocumentSnapshot) => doc.data() as T +}); diff --git a/src/utilities/js-utils.ts b/src/utilities/js-utils.ts index ce34db7b3c..96f84efd36 100644 --- a/src/utilities/js-utils.ts +++ b/src/utilities/js-utils.ts @@ -164,3 +164,13 @@ export function formatTimeZoneOffset(offset: number) { export function notEmpty(value: TValue | null | undefined): value is TValue { return value != null; } + +/** + * Compare the contents of two arrays, ignoring the order of the elements. + * @param a array + * @param b array + * @returns true if a and b have elements that compare equal, ignoring order + */ +export function arraysEqualIgnoringOrder(a: string[], b: string[]) { + return a.length === b.length && a.every((value) => b.includes(value)); +} diff --git a/src/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts new file mode 100644 index 0000000000..60175323b0 --- /dev/null +++ b/src/utilities/sort-document-utils.ts @@ -0,0 +1,176 @@ +import { IDocumentMetadata } from "../../shared/shared"; +import { FC, SVGProps } from "react"; +import { Bookmarks } from "src/models/stores/bookmarks"; +import { getTileComponentInfo } from "../models/tiles/tile-component-info"; + +import SparrowHeaderIcon from "../assets/icons/sort-by-tools/sparrow-id.svg"; + +export type DocumentCollection = { + label: string; + documents: IDocumentMetadata[]; + icon?: React.FC>; //exists only in the "sort by tools" case +} + +type TagWithDocs = { + tagKey: string; + tagValue: string; + docKeysFoundWithTag: string[]; +}; + +export const createDocMapByGroups = (documents: IDocumentMetadata[], groupForUser: (userId: string) => any) => { + const documentMap: Map = new Map(); + documents.forEach((doc) => { + const userId = doc.uid; + const group = groupForUser(userId); + const sectionLabel = group ? `Group ${group.id}` : "No Group"; + + if (!documentMap.has(sectionLabel)) { + documentMap.set(sectionLabel, []); + } + documentMap.get(sectionLabel)?.push(doc); + }); + return documentMap; +}; + +export const sortGroupSectionLabels = (docMapKeys: string[]) => { + return docMapKeys.sort((a, b) => { + const numA = parseInt(a.replace(/^\D+/g, ''), 10); + const numB = parseInt(b.replace(/^\D+/g, ''), 10); + return numA - numB; + }); +}; + +export const createDocMapByNames = (documents: IDocumentMetadata[], getUserById: (uid: string) => any) => { + const documentMap: Map = new Map(); + documents.forEach((doc) => { + const user = getUserById(doc.uid); + const sectionLabel = user && `${user.lastName}, ${user.firstName}`; + if (!documentMap.has(sectionLabel)) { + documentMap.set(sectionLabel, []); + } + documentMap.get(sectionLabel)?.push(doc); + }); + return documentMap; +}; + +export const sortNameSectionLabels = (docMapKeys: string[]) => { + return docMapKeys.sort((a, b) => { + const parseName = (name: any) => { + const [lastName, firstName] = name.split(", ").map((part: any) => part.trim()); + return { firstName, lastName }; + }; + const aParsed = parseName(a); + const bParsed = parseName(b); + + // Compare by last name, then by first name if last names are equal + const lastNameCompare = aParsed.lastName.localeCompare(bParsed.lastName); + if (lastNameCompare !== 0) { + return lastNameCompare; + } + return aParsed.firstName.localeCompare(bParsed.firstName); + }); +}; + +export const getTagsWithDocs = (documents: IDocumentMetadata[], commentTags: Record|undefined, + firestoreTagDocumentMap: Map>) => { + const tagsWithDocs: Record = {}; + if (commentTags) { + for (const key of Object.keys(commentTags)) { + tagsWithDocs[key] = { + tagKey: key, + tagValue: commentTags[key], + docKeysFoundWithTag: [] + }; + } + tagsWithDocs[""] = { //this accounts for when user commented with tagPrompt (no tag selected) + tagKey: "", + tagValue: "Not Tagged", + docKeysFoundWithTag: [] + }; + } + + // Find all unique document keys in tagsWithDocs. Compare this with all sortable documents + // in store to find "Documents with no comments" then place those doc keys to "Not Tagged" + const uniqueDocKeysWithTags = new Set(); + + // grouping documents based on firestore comment tags + firestoreTagDocumentMap.forEach((docKeysSet, tag) => { + const docKeysArray = Array.from(docKeysSet); // Convert the Set to an array + if (tagsWithDocs[tag]) { + docKeysSet.forEach((docKey: string) =>{ + uniqueDocKeysWithTags.add(docKey); + }); + tagsWithDocs[tag].docKeysFoundWithTag = docKeysArray; + } + }); + + // adding in (exemplar) documents with authored tags + documents.forEach(doc => { + doc.strategies?.forEach(strategy => { + if (tagsWithDocs[strategy]) { + tagsWithDocs[strategy].docKeysFoundWithTag.push(doc.key); + uniqueDocKeysWithTags.add(doc.key); + } + }); + }); + + documents.forEach(doc => { + if (!uniqueDocKeysWithTags.has(doc.key)) { + // This document has no comments + if (tagsWithDocs[""]) { + tagsWithDocs[""].docKeysFoundWithTag.push(doc.key); + } + } + }); + return tagsWithDocs; +}; + +export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => { + const toolToDocumentsMap = new Map>(); + const addDocByType = (docToAdd: IDocumentMetadata, type: string) => { + if (!toolToDocumentsMap.get(type)) { + let icon: FC> | undefined; + if (type === "Sparrow") { + icon = SparrowHeaderIcon; + } else { + const componentInfo = getTileComponentInfo(type); + icon = componentInfo?.HeaderIcon; + } + toolToDocumentsMap.set(type, { + icon, + documents: [] + } + ); + } + toolToDocumentsMap.get(type)?.documents.push(docToAdd); + }; + + //Iterate through all documents, determine if they are valid, + //create a map of valid ones, otherwise put them into the "No Tools" section + documents.forEach((doc) => { + if (doc.tools) { + const validTileTypes = doc.tools.filter(type => type !== "Placeholder" && type !== "Unknown"); + if (validTileTypes.length > 0) { + validTileTypes.forEach(tool => { + addDocByType(doc, tool); + }); + } else { + addDocByType(doc, "No Tools"); + } + } + }); + + return toolToDocumentsMap; +}; + +export const createDocMapByBookmarks = (documents: IDocumentMetadata[], bookmarks: Bookmarks) => { + const documentMap: Map = new Map(); + documents.forEach((doc) => { + const sectionLabel = bookmarks.isDocumentBookmarked(doc.key) ? "Bookmarked" : "Not Bookmarked"; + if (!documentMap.has(sectionLabel)) { + documentMap.set(sectionLabel, []); + } + documentMap.get(sectionLabel)?.push(doc); + }); + return documentMap; +}; diff --git a/src/utilities/url-params.ts b/src/utilities/url-params.ts index c9d6e0e4b0..eee2ec93e3 100644 --- a/src/utilities/url-params.ts +++ b/src/utilities/url-params.ts @@ -1,6 +1,5 @@ import { ParsedQuery, parse } from "query-string"; import { AppMode, AppModes } from "../models/stores/store-types"; -import { DBClearLevel } from "../lib/db"; export interface QueryParams { // appMode is "authed", "test" or "dev" with the default of dev @@ -79,8 +78,6 @@ export interface QueryParams { // group id for qa qaGroup?: string; - // db level to clear for qa - qaClear?: DBClearLevel; // direct firebase realtime database access to the emulator firebase?: string; // "emulator" or host:port url diff --git a/tsconfig.json b/tsconfig.json index 1e1192b878..348cd0160a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,6 @@ }, "types": ["w3c-web-serial", "gtag.js"] }, - "include": ["src/**/*"], + "include": ["src/**/*", "shared/*"], "exclude": ["**/node_modules", "**/.*/"] }