From e7dead8532b0cb70e92233063b948c249bcdfbfd Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 15 Jul 2024 18:32:12 -0400 Subject: [PATCH 001/127] feat: Show Docs Across Unit (PT-187899319) [187899319](https://www.pivotaltracker.com/story/show/187899319) --- src/clue/clue.sass | 2 + src/components/document/sort-work-view.scss | 13 +- src/components/document/sort-work-view.tsx | 62 +++++++--- src/components/navigation/nav-tab-panel.sass | 6 + .../navigation/sort-work-header-dropdown.scss | 61 ---------- .../navigation/sort-work-header.scss | 83 +++++++++++++ .../navigation/sort-work-header.tsx | 47 +++++--- src/components/themes.scss | 9 ++ .../thumbnail/simple-document-item.scss | 11 ++ .../thumbnail/simple-document-item.tsx | 32 +++++ src/models/document/document.ts | 4 +- src/models/stores/persistent-ui.ts | 6 +- src/models/stores/sorted-documents.ts | 111 ++++++++++++++---- src/models/stores/stores.ts | 1 + src/models/stores/ui-types.ts | 2 + 15 files changed, 329 insertions(+), 121 deletions(-) delete mode 100644 src/components/navigation/sort-work-header-dropdown.scss create mode 100644 src/components/navigation/sort-work-header.scss create mode 100644 src/components/thumbnail/simple-document-item.scss create mode 100644 src/components/thumbnail/simple-document-item.tsx diff --git a/src/clue/clue.sass b/src/clue/clue.sass index 30672fdb5f..b81a9bd470 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/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index dc0daf6847..f738bd378f 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -7,16 +7,21 @@ $title-margin: 2px; display: flex; flex-direction: column; - .sorted-sections{ + .tab-panel-documents-section { + border: solid 1.5px $classwork-purple; + border-top: none; + } + + .sorted-sections { width: 100%; - .section-header{ + .section-header { height: 30px; position: relative; margin-top: 5px; margin-bottom: 5px; - &::after{ //divider line drawn across + &::after { //divider line drawn across content: ""; position: absolute; left: 0px; @@ -25,7 +30,7 @@ $title-margin: 2px; border-bottom: 1px solid $charcoal-light-1; } - .section-header-label{ + .section-header-label { svg{ margin-right: 5px; } diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 5b2e1c99b8..0d2fab29f4 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -11,6 +11,8 @@ import { SortWorkDocumentArea } from "./sort-work-document-area"; import { ENavTab } from "../../models/view/nav-tabs"; import { DocListDebug } from "./doc-list-debug"; import { logDocumentViewEvent } from "../../models/document/log-document-event"; +import { SimpleDocumentItem } from "../thumbnail/simple-document-item"; +import { DocFilterType } from "../../models/stores/ui-types"; import "../thumbnail/document-type-collection.scss"; import "./sort-work-view.scss"; @@ -20,25 +22,41 @@ import "./sort-work-view.scss"; * 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 filterOptions: DocFilterType[] = ["Problem", "Investigation", "Unit", "All"]; const [sortBy, setSortBy] = useState("Group"); + const [docFilter, setDocFilter] = useState(persistentUIDocFilter); + + const handleDocFilterSelection = (filter: DocFilterType) => { + sortedDocuments.setDocFilter(filter); + persistentUI.setDocFilter(filter); + setDocFilter(filter); + }; useEffect(()=>{ + sortedDocuments.setDocFilter(docFilter); if (sortBy === sortTagPrompt){ sortedDocuments.updateTagDocumentMap(); } - },[sortedDocuments, sortBy, sortTagPrompt]); + sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); + },[sortedDocuments, sortBy, sortTagPrompt, docFilter, investigation, unit, problem]); - const sortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + const primarySortItems: ICustomDropdownItem[] = sortOptions.map((option) => ({ + selected: option === sortBy, text: option, onClick: () => setSortBy(option) })); + const filterItems: ICustomDropdownItem[] = filterOptions.map((option) => ({ + selected: option === docFilter, + text: option, + onClick: () => handleDocFilterSelection(option) + })); + let renderedSortedDocuments; switch (sortBy) { case "Group": @@ -58,7 +76,6 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { break; } - //******************************* Click to Open Document *************************************** const handleSelectDocument = (document: DocumentModelType) => { persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); logDocumentViewEvent(document); @@ -74,7 +91,12 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { showSortWorkDocumentArea ? : <> - +
{ renderedSortedDocuments && renderedSortedDocuments.map((sortedSection, idx) => { @@ -90,15 +112,21 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { const documentContext = getDocumentContext(doc); return ( - + {docFilter === "Problem" + ? + : } ); })} 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/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..8ad06a8303 --- /dev/null +++ b/src/components/navigation/sort-work-header.scss @@ -0,0 +1,83 @@ +@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; + } + + .header-dropdown { + display: flex; + align-items: center; + + .custom-select.sort-work-sort-menu, + .custom-select.filter-work-menu { + color: $charcoal-dark-2; + margin-right: 8px; + + .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..b87d18f120 100644 --- a/src/components/navigation/sort-work-header.tsx +++ b/src/components/navigation/sort-work-header.tsx @@ -1,26 +1,43 @@ 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[] + filter: string; + filterItems: ICustomDropdownItem[]; + primarySort: string; + primarySortItems: ICustomDropdownItem[]; } -export const SortWorkHeader:React.FC= observer(function SortWorkView({sortBy, sortByOptions}){ +export const SortWorkHeader:React.FC= observer(function SortWorkView(props){ + const { filter, filterItems, primarySort, primarySortItems } = props; return ( -
-
Sort by
-
- +
+
+
Sort by
+
+ +
+
+
+
Show for
+
+ +
); 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/simple-document-item.scss b/src/components/thumbnail/simple-document-item.scss new file mode 100644 index 0000000000..5371b4242c --- /dev/null +++ b/src/components/thumbnail/simple-document-item.scss @@ -0,0 +1,11 @@ +@import "../vars"; + +.simple-document-item { + background: $classwork-purple-light-7; + border: solid 1px #707070; + border-radius: 1px; + cursor: pointer; + height: 12px; + margin: 2px 5px; + width: 12px; +} diff --git a/src/components/thumbnail/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx new file mode 100644 index 0000000000..309b0c7ac3 --- /dev/null +++ b/src/components/thumbnail/simple-document-item.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { useStores } from "../../hooks/use-stores"; +import { DocumentModelType } from "../../models/document/document"; + +import "./simple-document-item.scss"; + +interface IProps { + document: DocumentModelType; + investigationOrdinal: number; + problemOrdinal: number; + onSelectDocument: (document: DocumentModelType) => void; +} + +export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDocument, problemOrdinal }: IProps) => { + const { class: classStore, unit } = useStores(); + const { uid } = document; + const userName = classStore.getUserById(uid)?.displayName; + const investigations = unit.investigations; + const investigation = investigations[investigationOrdinal]; + const problem = investigation?.problems[problemOrdinal - 1]; + const title = document.title ? `${userName}: ${document.title}` : `${userName}: ${problem?.title ?? "unknown title"}`; + // TODO: Account for isPrivate. The below call to isAccessibleToUser won't currently work here. + // const isPrivate = !document.isAccessibleToUser(user, documents); + + const handleClick = () => { + onSelectDocument(document); + }; + + return ( +
+ ); +}; diff --git a/src/models/document/document.ts b/src/models/document/document.ts index d30c344c18..f673f811a0 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -54,7 +54,9 @@ 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))), + strategies: types.maybe(types.array(types.string)), + tileTypes: types.maybe(types.array(types.string)) }) .volatile(self => ({ treeMonitor: undefined as TreeMonitor | undefined, diff --git a/src/models/stores/persistent-ui.ts b/src/models/stores/persistent-ui.ts index 599c3b8a8d..65389861bb 100644 --- a/src/models/stores/persistent-ui.ts +++ b/src/models/stores/persistent-ui.ts @@ -2,7 +2,7 @@ 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"; @@ -36,6 +36,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, @@ -188,6 +189,9 @@ export const PersistentUIModel = types }, setProblemPath(problemPath: string) { self.problemPath = problemPath; + }, + setDocFilter(docFilter: DocFilterType) { + self.docFilter = docFilter; } }; }) diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 126233d32d..976e504a35 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -11,6 +11,7 @@ import { ENavTabOrder, NavTabSectionModelType } from "../view/nav-tabs"; import { UserModelType } from "./user"; import { getTileContentInfo } from "../tiles/tile-content-info"; import { getTileComponentInfo } from "../tiles/tile-component-info"; +import { DocFilterType } from "./ui-types"; import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; @@ -33,6 +34,7 @@ export interface ISortedDocumentsStores { db: DB; appConfig: AppConfigModelType; bookmarks: Bookmarks; + docFilter: DocFilterType; user: UserModelType; } @@ -42,6 +44,7 @@ interface IMatchPropertiesOptions { export class SortedDocuments { stores: ISortedDocumentsStores; firestoreTagDocumentMap = new Map>(); + firestoreMetadataDocs: any = []; constructor(stores: ISortedDocumentsStores) { makeAutoObservable(this); @@ -72,7 +75,8 @@ export class SortedDocuments { } get filteredDocsByType(): DocumentModelType[] { - return this.documents.all.filter((doc: DocumentModelType) => { + const documents = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; + return documents.filter((doc: DocumentModelType) => { return isSortableType(doc.type); }); } @@ -172,12 +176,21 @@ export class SortedDocuments { // 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); + if (this.stores.docFilter === "Problem") { + const foundTagKey = doc.getProperty("authoredCommentTag"); + if (foundTagKey !== undefined && foundTagKey !== "") { + if (tagsWithDocs[foundTagKey]) { + tagsWithDocs[foundTagKey].docKeysFoundWithTag.push(doc.key); + uniqueDocKeysWithTags.add(doc.key); + } } + } else { + doc.strategies?.forEach(strategy => { + if (tagsWithDocs[strategy]) { + tagsWithDocs[strategy].docKeysFoundWithTag.push(doc.key); + uniqueDocKeysWithTags.add(doc.key); + } + }); } }); @@ -195,7 +208,8 @@ export class SortedDocuments { const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; const sectionLabel = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; - const documents = this.documents.all.filter(doc => docKeys.includes(doc.key)); + const docs = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; + const documents = docs.filter((doc: any) => docKeys.includes(doc.key)); sortedDocsArr.push({ sectionLabel, documents @@ -231,6 +245,41 @@ export class SortedDocuments { }); } + async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { + const db = this.db.firestore; + let query = db.collection("documents").where("context_id", "==", this.user.classHash); + + if (filter !== "All") { + query = query.where("unit" , "==", unit); + } + if (filter === "Investigation" || filter === "Problem") { + query = query.where("investigation", "==", String(investigation)); + } + if (filter === "Problem") { + query = query.where("problem", "==", String(problem)); + } + const queryForUnitNull = db.collection("documents").where("context_id", "==", this.user.classHash) + .where("unit" , "==", null); + const [docsWithUnit, docsWithoutUnit] = await Promise.all([query.get(), queryForUnitNull.get()]); + const docsArray: any = []; + + 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); + }); + + runInAction(() => { + this.firestoreMetadataDocs.replace(docsArray); + }); + } + //*************************************** Sort By Bookmarks ************************************* get sortByBookmarks(): SortedDocument[] { @@ -265,22 +314,36 @@ export class SortedDocuments { //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"); + if (this.stores.docFilter === "Problem") { + 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"); + } + } + } else { + if (doc.tileTypes) { + const validTileTypes = doc.tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); + if (validTileTypes.length > 0) { + validTileTypes.forEach(tileType => { + addDocByType(doc, tileType); + }); + // TODO: Sparrow annotations. We'll first need to add information about these to metadata docs. + } else { + addDocByType(doc, "No Tools"); } - } else { //Documents with only all Placeholder or Unknown tiles - addDocByType(doc, "No Tools"); } } }); @@ -383,4 +446,8 @@ export class SortedDocuments { return sectDocs; } + setDocFilter(filter: DocFilterType) { + this.stores.docFilter = filter; + } + } diff --git a/src/models/stores/stores.ts b/src/models/stores/stores.ts index f371cc62e8..6c6259c31f 100644 --- a/src/models/stores/stores.ts +++ b/src/models/stores/stores.ts @@ -78,6 +78,7 @@ class Stores implements IStores{ ui: UIModelType; groups: GroupsModelType; class: ClassModelType; + docFilter: "Problem" | "Investigation" | "Unit" = "Problem"; documents: DocumentsModelType; networkDocuments: DocumentsModelType; db: DB; diff --git a/src/models/stores/ui-types.ts b/src/models/stores/ui-types.ts index 606c1d0ad8..9806ccc34a 100644 --- a/src/models/stores/ui-types.ts +++ b/src/models/stores/ui-types.ts @@ -2,6 +2,8 @@ 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 const kDividerMin = 0; // left side (resources/navigation) is collapsed export const kDividerHalf = 50; // resources/navigation and workspace are split 50/50 From 3cf065237d8f189eaf8740b52eb788219685e364 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 22 Jul 2024 15:20:24 -0400 Subject: [PATCH 002/127] chore: improve types, minor clean up --- src/components/document/sort-work-view.tsx | 5 ++--- src/components/thumbnail/simple-document-item.tsx | 2 +- src/models/stores/sorted-documents.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 53402b7e5f..07833d8eb8 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -39,8 +39,7 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); },[sortedDocuments, sortBy, sortTagPrompt, docFilter, investigation, unit, problem]); - const primarySortItems: ICustomDropdownItem[] = sortOptions.map((option) => ({ - selected: option === sortBy, + const sortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ text: option, onClick: () => setSortBy(option) })); @@ -84,7 +83,7 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { filter={docFilter} filterItems={filterItems} primarySort={sortBy} - primarySortItems={primarySortItems} + primarySortItems={sortByOptions} />
{ renderedSortedDocuments && diff --git a/src/components/thumbnail/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx index 309b0c7ac3..00f356df57 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -19,7 +19,7 @@ export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDoc const investigation = investigations[investigationOrdinal]; const problem = investigation?.problems[problemOrdinal - 1]; const title = document.title ? `${userName}: ${document.title}` : `${userName}: ${problem?.title ?? "unknown title"}`; - // TODO: Account for isPrivate. The below call to isAccessibleToUser won't currently work here. + // TODO: Account for and use isPrivate in the view. isAccessibleToUser won't currently work here. // const isPrivate = !document.isAccessibleToUser(user, documents); const handleClick = () => { diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index b65e235e55..6d34dbb04a 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -1,4 +1,4 @@ -import { ObservableSet, makeAutoObservable, runInAction } from "mobx"; +import { ObservableSet, makeAutoObservable, runInAction, IObservableArray, observable } from "mobx"; import { DocumentModelType } from "../document/document"; import { isPublishedType, isSortableType, isUnpublishedType } from "../document/document-types"; import { DocumentsModelType } from "./documents"; @@ -44,7 +44,7 @@ interface IMatchPropertiesOptions { export class SortedDocuments { stores: ISortedDocumentsStores; firestoreTagDocumentMap = new Map>(); - firestoreMetadataDocs: any = []; + firestoreMetadataDocs: IObservableArray = observable.array([]); constructor(stores: ISortedDocumentsStores) { makeAutoObservable(this); @@ -209,7 +209,7 @@ export class SortedDocuments { const sectionLabel = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; const docs = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; - const documents = docs.filter((doc: any) => docKeys.includes(doc.key)); + const documents = docs.filter((doc: DocumentModelType) => docKeys.includes(doc.key)); sortedDocsArr.push({ sectionLabel, documents @@ -261,17 +261,17 @@ export class SortedDocuments { const queryForUnitNull = db.collection("documents").where("context_id", "==", this.user.classHash) .where("unit" , "==", null); const [docsWithUnit, docsWithoutUnit] = await Promise.all([query.get(), queryForUnitNull.get()]); - const docsArray: any = []; + const docsArray: DocumentModelType[] = []; const matchedDocKeys = new Set(); docsWithUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push(doc.data()); + docsArray.push(doc.data() as DocumentModelType); matchedDocKeys.add(doc.data().key); }); docsWithoutUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push(doc.data()); + docsArray.push(doc.data() as DocumentModelType); matchedDocKeys.add(doc.data().key); }); From 0f63f581d0c96cb1a0d22daeadc786c6bbc011e7 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 22 Jul 2024 16:38:53 -0400 Subject: [PATCH 003/127] chore: update test coverage --- .../teacher_sort_work_view_spec.js | 22 +++++++++++++++++++ cypress/support/elements/common/SortedWork.js | 18 +++++++++++++++ .../thumbnail/simple-document-item.tsx | 8 ++++++- src/models/stores/sorted-documents.test.ts | 1 + src/models/stores/sorted-documents.ts | 5 +++-- 5 files changed, 51 insertions(+), 3 deletions(-) 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..289c68e9bd 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 @@ -66,6 +66,28 @@ describe('SortWorkView Tests', () => { sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed }); + it.only("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, the documents should be listed using the larger thumbnail view + // [data-test=sort-work-list-items] should have a length greater than 0 + 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); + cy.get("[data-test=sort-work-list-items]").should("not.exist"); + cy.get("[data-test=simple-document-item]").should("have.length.greaterThan", 0); + }); + 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 diff --git a/cypress/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index 07bd02f01e..be2e7e20ad 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -20,6 +20,24 @@ class SortedWork { getSortWorkGroup(groupName) { return cy.get(".sort-work-view .sorted-sections .section-header-label").contains(groupName).parent().parent().parent(); } + 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-problem]"); + } + getShowForUnitOption() { + return cy.get("[data-test=list-item-unit]"); + } + getShowForAllOption() { + return cy.get("[data-test=list-item-all]"); + } + getShowForMenuOption(level) { + return cy.get("[data-test=filter-work-menu-list]").contains(level); + } 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}); } diff --git a/src/components/thumbnail/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx index 00f356df57..a22471c07d 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -27,6 +27,12 @@ export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDoc }; return ( -
+
+
); }; diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index d1240be38e..8e9f131695 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -131,6 +131,7 @@ describe('Sorted Documents Model', () => { documents: { all: mockDocuments }, groups: mockGroups, class: mockClass, + docFilter: "Problem" }; sortedDocuments = new SortedDocuments(mockStores as ISortedDocumentsStores); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 6d34dbb04a..4b4044cb3a 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -264,14 +264,15 @@ export class SortedDocuments { const docsArray: DocumentModelType[] = []; const matchedDocKeys = new Set(); + const propertiesPlaceholder = new Map(); docsWithUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push(doc.data() as DocumentModelType); + docsArray.push({...doc.data(), properties: propertiesPlaceholder} as DocumentModelType); matchedDocKeys.add(doc.data().key); }); docsWithoutUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push(doc.data() as DocumentModelType); + docsArray.push({...doc.data(), properties: propertiesPlaceholder} as DocumentModelType); matchedDocKeys.add(doc.data().key); }); From c6b2a072449aeb9d924eff9094e2ca028308a6a7 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 23 Jul 2024 16:17:26 -0400 Subject: [PATCH 004/127] chore: use metadata instead of full docs --- functions/src/shared.ts | 5 + scripts/ai/download-documents-with-info.ts | 4 +- scripts/ai/get-document-bookmarks.ts | 48 +++++ src/components/document/doc-list-debug.tsx | 9 +- .../document/document-file-menu.tsx | 2 +- .../document/sort-work-document-area.tsx | 29 ++- src/components/document/sorted-documents.tsx | 93 ++++++--- .../thumbnail/documents-type-collection.tsx | 4 +- .../thumbnail/simple-document-item.tsx | 6 +- src/hooks/firestore-hooks.ts | 11 +- src/lib/db.ts | 9 +- src/models/document/document-utils.ts | 11 +- src/models/document/document.ts | 8 +- src/models/stores/documents.ts | 15 ++ src/models/stores/section-docs-store.ts | 100 ++++++++++ src/models/stores/sorted-documents.ts | 184 ++++++------------ src/models/stores/stores.ts | 4 + src/utilities/db-utils.ts | 9 + 18 files changed, 372 insertions(+), 179 deletions(-) create mode 100644 scripts/ai/get-document-bookmarks.ts create mode 100644 src/models/stores/section-docs-store.ts create mode 100644 src/utilities/db-utils.ts diff --git a/functions/src/shared.ts b/functions/src/shared.ts index 642a8f3354..599f02737e 100644 --- a/functions/src/shared.ts +++ b/functions/src/shared.ts @@ -108,6 +108,11 @@ export interface IDocumentMetadata { title?: string; originDoc?: string; properties?: Record; + tileTypes?: string[]; + strategies?: string[]; + investigation?: string; + problem?: string; + unit?: string; } export function isDocumentMetadata(o: any): o is IDocumentMetadata { return !!o.uid && !!o.type && !!o.key; diff --git a/scripts/ai/download-documents-with-info.ts b/scripts/ai/download-documents-with-info.ts index 52ee0a23c8..326bb9e3cf 100644 --- a/scripts/ai/download-documents-with-info.ts +++ b/scripts/ai/download-documents-with-info.ts @@ -22,8 +22,8 @@ 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; +const demo = "TAGCLUE"; +// const demo = false; // Make falsy to include all documents const documentLimit = false; diff --git a/scripts/ai/get-document-bookmarks.ts b/scripts/ai/get-document-bookmarks.ts new file mode 100644 index 0000000000..38fd459e66 --- /dev/null +++ b/scripts/ai/get-document-bookmarks.ts @@ -0,0 +1,48 @@ +#!/usr/bin/node + +// This script gets bookmark info for each document + +// Possible References: +// src/lib/db-listeners/db-bookmarks-listener.ts, src/lib/firebase.ts, +// src/models/stores/bookmarks.ts, src/models/stores/stores.ts + +import fs from "fs"; +import admin from "firebase-admin"; + +import { DocumentInfo } from "./script-types"; +import { datasetPath, networkFileName } from "./script-constants"; + +const sourceDirectory = ""; +const queryLimit = 10; +const startTime = Date.now(); +const documentInfo: Record = {}; + +const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; + +// Fetch the service account key JSON file contents; must be in same folder as script +const credential = admin.credential.cert('./serviceAccountKey.json'); +// Initialize the app with a service account, granting admin privileges +admin.initializeApp({ + credential, + databaseURL +}); + +const credentialTime = Date.now(); + +const sourcePath = `${datasetPath}${sourceDirectory}`; + +// Get network info from portal file. This should have been created by download-documents.ts. +function getNetworkInfo() { + const networkFile = `${sourcePath}/${networkFileName}`; + if (fs.existsSync(networkFile)) { + return JSON.parse(fs.readFileSync(networkFile, "utf8")); + } +} +const { portal, demo } = getNetworkInfo() ?? { portal: "learn.concord.org" }; + +// in src/lib/firebase.ts: +// public getUserDocumentStarsPath(user: UserModelType, documentKey?: string, starKey?: string) { +// const docSuffix = documentKey ? `/${documentKey}` : ""; +// const starSuffix = starKey ? `/${starKey}` : ""; +// return `${this.getOfferingPath(user)}/commentaries/stars${docSuffix}${starSuffix}`; +// } diff --git a/src/components/document/doc-list-debug.tsx b/src/components/document/doc-list-debug.tsx index 85c8b9d13e..3eda15fbaa 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 "../../../functions/src/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/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/sorted-documents.tsx b/src/components/document/sorted-documents.tsx index b0f309750b..3544a15ee7 100644 --- a/src/components/document/sorted-documents.tsx +++ b/src/components/document/sorted-documents.tsx @@ -1,16 +1,19 @@ 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 { DocumentContextReact } from "./document-context"; import { SortedDocument } from "../../models/stores/sorted-documents"; -import { DocumentModelType, getDocumentContext } from "../../models/document/document"; +import { DocumentModelType, getDocumentContext, isDocumentModel } 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 { DocFilterType } from "../../models/stores/ui-types"; import { SimpleDocumentItem } from "../thumbnail/simple-document-item"; +import { IDocumentMetadata } from "../../../functions/src/shared"; + +import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; import "./sort-work-view.scss"; @@ -22,19 +25,75 @@ interface IProps { export const SortedDocuments: React.FC = observer(function SortedDocuments(props: IProps) { const { docFilter, idx, sortedSection } = props; - const { persistentUI } = useStores(); - + const { persistentUI, sortedDocuments } = useStores(); const [showDocuments, setShowDocuments] = useState(false); - const handleSelectDocument = (document: DocumentModelType) => { - persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); - logDocumentViewEvent(document); + 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 its an observer. + sortedDocuments.fetchFullDocument(docKey); + + return undefined; + }; + + const documentCount = () => { + if (docFilter !== "Problem") return sortedSection.documents.length; + + const downloadedDocs = sortedSection.documents.filter(doc => { + const exists = getDocument(doc.key); + return exists; + }); + return downloadedDocs.length; + }; + + const getDocumentFromMetadata = (docKey: string) => { + return sortedDocuments.fetchFullDocument(docKey); + }; + + const handleSelectDocument = async (document: DocumentModelType | IDocumentMetadata) => { + try { + const doc = isDocumentModel(document) + ? getDocument(document.key) + : getDocument(document.key) || await getDocumentFromMetadata(document.key); + persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); + if (doc) { + logDocumentViewEvent(doc as DocumentModelType); + } + } catch (e) { + console.error("Error selecting document", e); + } }; const handleToggleShowDocuments = () => { setShowDocuments(!showDocuments); }; + const renderDocumentItem = (doc: any) => { + const fullDocument = getDocument(doc.key); + if(docFilter === "Problem" && fullDocument) { + return ; + } else if (docFilter !== "Problem") { + return ; + } else { + return null; + } + }; + return (
@@ -43,7 +102,7 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen {sortedSection.icon ? : null} {sortedSection.sectionLabel}
-
Total workspaces: {sortedSection.documents.length}
+
Total workspaces: {documentCount()}
= observer(function SortedDocumen const documentContext = getDocumentContext(doc); return ( - {docFilter === "Problem" - ? - : } + {renderDocumentItem(doc)} ); })} 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.tsx b/src/components/thumbnail/simple-document-item.tsx index a22471c07d..2d5cc05cad 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -1,14 +1,14 @@ import React from "react"; +import { IDocumentMetadata } from "../../../functions/src/shared"; import { useStores } from "../../hooks/use-stores"; -import { DocumentModelType } from "../../models/document/document"; import "./simple-document-item.scss"; interface IProps { - document: DocumentModelType; + document: IDocumentMetadata; investigationOrdinal: number; problemOrdinal: number; - onSelectDocument: (document: DocumentModelType) => void; + onSelectDocument: (document: IDocumentMetadata) => void; } export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDocument, problemOrdinal }: IProps) => { 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/lib/db.ts b/src/lib/db.ts index 590511074f..3a832d1f26 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -87,6 +87,8 @@ export interface OpenDocumentOptions { groupUserConnections?: Record; originDoc?: string; pubVersion?: number; + problemOrdinal?: string; + unit?: string; } export class DB { @@ -544,7 +546,8 @@ export class DB { public openDocument(options: OpenDocumentOptions) { const { documents } = this.stores; - const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion} = options; + const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion, + problemOrdinal, unit} = options; return new Promise((resolve, reject) => { const {user} = this.stores; const documentPath = this.firebase.getUserDocumentPath(user, documentKey, userId); @@ -587,7 +590,9 @@ export class DB { createdAt: metadata.createdAt, content: content ? content : {}, changeCount: document.changeCount, - pubVersion + pubVersion, + problemOrdinal, + unit }); } catch (e) { const msg = "Could not open " + diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index 018e5c48cb..ebb55c723e 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -8,15 +8,20 @@ import { DocumentContentModelType } from "./document-content"; import { isPlanningType, isProblemType } from "./document-types"; export function getDocumentDisplayTitle( - document: DocumentModelType, appConfig: AppConfigModelType, problem: ProblemModelType + document: DocumentModelType, appConfig: AppConfigModelType, problem?: ProblemModelType, + unit?: string ) { const { type } = document; + const problemTitle = !(document.problemOrdinal || document.unit) || + (document.problemOrdinal === 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); } diff --git a/src/models/document/document.ts b/src/models/document/document.ts index f673f811a0..d279321857 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -55,8 +55,8 @@ export const DocumentModel = Tree.named("Document") changeCount: types.optional(types.number, 0), pubVersion: types.maybe(types.number), supportContentType: types.maybe(types.enumeration("SupportType", Object.values(ESupportType))), - strategies: types.maybe(types.array(types.string)), - tileTypes: types.maybe(types.array(types.string)) + problemOrdinal: types.maybe(types.string), + unit: types.maybe(types.string), }) .volatile(self => ({ treeMonitor: undefined as TreeMonitor | undefined, @@ -317,6 +317,10 @@ export const DocumentModel = Tree.named("Document") export type DocumentModelType = Instance; export type DocumentModelSnapshotType = SnapshotIn; +export const isDocumentModel = (document: any): document is DocumentModelType => { + return DocumentModel.is(document); +}; + export const getDocumentContext = (document: DocumentModelType): IDocumentContext => { const { type, key, title, originDoc } = document; return { diff --git a/src/models/stores/documents.ts b/src/models/stores/documents.ts index 1a7aa7ed74..eac20aab4d 100644 --- a/src/models/stores/documents.ts +++ b/src/models/stores/documents.ts @@ -50,6 +50,21 @@ export const DocumentsModel = types getDocument(documentKey: string) { return self.all.find((document) => document.key === documentKey); }, + async fetchDocument(documentKey: string) { + let doc = self.all.find((document) => document.key === documentKey); + if (!doc) { + const docSnapshot = await self.firestore?.collection("documents").where("key", "==", documentKey) + .where("context_id", "==", self.userContextProvider?.userContext.classHash).get(); + if (docSnapshot?.docs.length) { + const _doc = docSnapshot?.docs[0].data(); + const key = _doc?.data().key; + const type = _doc?.data().type; + const uid = _doc?.data().uid; + doc = {..._doc.data(), key, type, uid}; + } + } + return doc; + }, byType(type: DocumentType) { return self.all.filter((document) => document.type === type); 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.ts b/src/models/stores/sorted-documents.ts index 4b4044cb3a..68de62d36b 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -1,23 +1,23 @@ import { ObservableSet, makeAutoObservable, runInAction, IObservableArray, observable } from "mobx"; -import { DocumentModelType } from "../document/document"; -import { isPublishedType, isSortableType, isUnpublishedType } from "../document/document-types"; +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 { getTileContentInfo } from "../tiles/tile-content-info"; import { getTileComponentInfo } from "../tiles/tile-component-info"; import { DocFilterType } from "./ui-types"; +import { IDocumentMetadata } from "../../../functions/src/shared"; +import { typeConverter } from "../../utilities/db-utils"; import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; export type SortedDocument = { sectionLabel: string; - documents: DocumentModelType[]; + documents: IDocumentMetadata[]; icon?: React.FC>; //exists only in the "sort by tools" case } @@ -38,13 +38,10 @@ export interface ISortedDocumentsStores { user: UserModelType; } -interface IMatchPropertiesOptions { - isTeacherDocument?: boolean; -} export class SortedDocuments { stores: ISortedDocumentsStores; firestoreTagDocumentMap = new Map>(); - firestoreMetadataDocs: IObservableArray = observable.array([]); + firestoreMetadataDocs: IObservableArray = observable.array([]); constructor(stores: ISortedDocumentsStores) { makeAutoObservable(this); @@ -74,9 +71,14 @@ export class SortedDocuments { return this.stores.user; } - get filteredDocsByType(): DocumentModelType[] { - const documents = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; - return documents.filter((doc: DocumentModelType) => { + + // The error is occurring because the this.firestoreMetadataDocs array is of type + // IObservableArray, but the filteredDocsByType getter is returning + // an array of type IDocumentMetadata[]. To fix this, you can convert the IObservableArray + // to a regular array using the slice() method. + get filteredDocsByType(): IDocumentMetadata[] { + // const documents = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; + return this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => { return isSortableType(doc.type); }); } @@ -176,22 +178,22 @@ export class SortedDocuments { // adding in (exemplar) documents with authored tags const allSortableDocKeys = this.filteredDocsByType; allSortableDocKeys.forEach(doc => { - if (this.stores.docFilter === "Problem") { - const foundTagKey = doc.getProperty("authoredCommentTag"); - if (foundTagKey !== undefined && foundTagKey !== "") { - if (tagsWithDocs[foundTagKey]) { - tagsWithDocs[foundTagKey].docKeysFoundWithTag.push(doc.key); - uniqueDocKeysWithTags.add(doc.key); - } - } - } else { + // if (this.stores.docFilter === "Problem") { + // const foundTagKey = doc.getProperty("authoredCommentTag"); + // if (foundTagKey !== undefined && foundTagKey !== "") { + // if (tagsWithDocs[foundTagKey]) { + // tagsWithDocs[foundTagKey].docKeysFoundWithTag.push(doc.key); + // uniqueDocKeysWithTags.add(doc.key); + // } + // } + // } else { doc.strategies?.forEach(strategy => { if (tagsWithDocs[strategy]) { tagsWithDocs[strategy].docKeysFoundWithTag.push(doc.key); uniqueDocKeysWithTags.add(doc.key); } }); - } + //} }); allSortableDocKeys.forEach(doc => { @@ -208,8 +210,8 @@ export class SortedDocuments { const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; const sectionLabel = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; - const docs = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; - const documents = docs.filter((doc: DocumentModelType) => docKeys.includes(doc.key)); + // const docs = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; + const documents = this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); sortedDocsArr.push({ sectionLabel, documents @@ -247,7 +249,8 @@ export class SortedDocuments { async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { const db = this.db.firestore; - let query = db.collection("documents").where("context_id", "==", this.user.classHash); + const converter = typeConverter(); + let query = db.collection("documents").withConverter(converter).where("context_id", "==", this.user.classHash); if (filter !== "All") { query = query.where("unit" , "==", unit); @@ -258,21 +261,21 @@ export class SortedDocuments { if (filter === "Problem") { query = query.where("problem", "==", String(problem)); } - const queryForUnitNull = db.collection("documents").where("context_id", "==", this.user.classHash) + 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: DocumentModelType[] = []; + const docsArray: IDocumentMetadata[] = []; const matchedDocKeys = new Set(); - const propertiesPlaceholder = new Map(); docsWithUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push({...doc.data(), properties: propertiesPlaceholder} as DocumentModelType); + docsArray.push(doc.data()); matchedDocKeys.add(doc.data().key); }); docsWithoutUnit.docs.forEach(doc => { if (matchedDocKeys.has(doc.data().key)) return; - docsArray.push({...doc.data(), properties: propertiesPlaceholder} as DocumentModelType); + docsArray.push(doc.data()); matchedDocKeys.add(doc.data().key); }); @@ -281,6 +284,32 @@ export class SortedDocuments { }); } + async fetchFullDocument(docKey: string) { + const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); + if (!metadataDoc) return; + + const problemOrdinal = metadataDoc?.type === "problem" + ? `${metadataDoc?.investigation}.${metadataDoc?.problem}` + : undefined; + const unit = metadataDoc?.type === "problem" ? metadataDoc?.unit : undefined; + + const props = { + documentKey: metadataDoc?.key, + type: metadataDoc?.type as any, + title: metadataDoc?.title || undefined, + properties: metadataDoc?.properties, + userId: metadataDoc?.uid, + groupId: undefined, + visibility: undefined, + originDoc: undefined, + pubVersion: undefined, + problemOrdinal, + unit, + }; + + return this.db.openDocument(props); + } + //*************************************** Sort By Bookmarks ************************************* get sortByBookmarks(): SortedDocument[] { @@ -303,9 +332,9 @@ export class SortedDocuments { //**************************************** Sort By Tools **************************************** get sortByTools(): SortedDocument[] { - const tileTypeToDocumentsMap: Record = {}; + const tileTypeToDocumentsMap: Record = {}; - const addDocByType = (docToAdd: DocumentModelType, type: string) => { + const addDocByType = (docToAdd: IDocumentMetadata, type: string) => { if (!tileTypeToDocumentsMap[type]) { tileTypeToDocumentsMap[type] = []; } @@ -315,26 +344,6 @@ export class SortedDocuments { //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) => { - if (this.stores.docFilter === "Problem") { - 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"); - } - } - } else { if (doc.tileTypes) { const validTileTypes = doc.tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); if (validTileTypes.length > 0) { @@ -346,7 +355,7 @@ export class SortedDocuments { addDocByType(doc, "No Tools"); } } - } + //} }); // Map the tile types to their display names @@ -376,77 +385,6 @@ export class SortedDocuments { 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; - }); - } - - isMatchingSpec(doc: DocumentModelType, type: string, properties?: readonly string[]) { - return (type === doc.type) && this.matchProperties(doc, properties); - } - - isTeacherDocument(doc: DocumentModelType){ - return this.class.isTeacher(doc.uid); - } - - 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(); - } - // 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; - } - setDocFilter(filter: DocFilterType) { this.stores.docFilter = filter; } diff --git a/src/models/stores/stores.ts b/src/models/stores/stores.ts index 6c6259c31f..fa4422dde6 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; @@ -91,6 +93,7 @@ class Stores implements IStores{ serialDevice: SerialDevice; userContextProvider: UserContextProvider; sortedDocuments: SortedDocuments; + sectionDocuments: SectionDocuments; unitLoadedPromise: Promise; sectionsLoadedPromise: Promise; startedLoadingUnitAndProblem: boolean; @@ -151,6 +154,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); 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 +}); From d0764f87c6b37db2ec7e9a6a215b77016136897f Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 23 Jul 2024 17:48:57 -0400 Subject: [PATCH 005/127] chore: minor refactor/clean up --- scripts/ai/download-documents-with-info.ts | 4 +- scripts/ai/get-document-bookmarks.ts | 48 ------------------- src/components/document/sort-work-view.tsx | 6 +-- src/components/document/sorted-documents.tsx | 24 ++++------ .../navigation/sort-work-header.tsx | 10 ++-- src/models/document/document.ts | 4 -- src/models/stores/documents.ts | 13 ++--- src/models/stores/sorted-documents.ts | 30 +++--------- 8 files changed, 32 insertions(+), 107 deletions(-) delete mode 100644 scripts/ai/get-document-bookmarks.ts diff --git a/scripts/ai/download-documents-with-info.ts b/scripts/ai/download-documents-with-info.ts index 326bb9e3cf..52ee0a23c8 100644 --- a/scripts/ai/download-documents-with-info.ts +++ b/scripts/ai/download-documents-with-info.ts @@ -22,8 +22,8 @@ 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; +// const demo = "TAGCLUE"; +const demo = false; // Make falsy to include all documents const documentLimit = false; diff --git a/scripts/ai/get-document-bookmarks.ts b/scripts/ai/get-document-bookmarks.ts deleted file mode 100644 index 38fd459e66..0000000000 --- a/scripts/ai/get-document-bookmarks.ts +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/node - -// This script gets bookmark info for each document - -// Possible References: -// src/lib/db-listeners/db-bookmarks-listener.ts, src/lib/firebase.ts, -// src/models/stores/bookmarks.ts, src/models/stores/stores.ts - -import fs from "fs"; -import admin from "firebase-admin"; - -import { DocumentInfo } from "./script-types"; -import { datasetPath, networkFileName } from "./script-constants"; - -const sourceDirectory = ""; -const queryLimit = 10; -const startTime = Date.now(); -const documentInfo: Record = {}; - -const databaseURL = "https://collaborative-learning-ec215.firebaseio.com"; - -// Fetch the service account key JSON file contents; must be in same folder as script -const credential = admin.credential.cert('./serviceAccountKey.json'); -// Initialize the app with a service account, granting admin privileges -admin.initializeApp({ - credential, - databaseURL -}); - -const credentialTime = Date.now(); - -const sourcePath = `${datasetPath}${sourceDirectory}`; - -// Get network info from portal file. This should have been created by download-documents.ts. -function getNetworkInfo() { - const networkFile = `${sourcePath}/${networkFileName}`; - if (fs.existsSync(networkFile)) { - return JSON.parse(fs.readFileSync(networkFile, "utf8")); - } -} -const { portal, demo } = getNetworkInfo() ?? { portal: "learn.concord.org" }; - -// in src/lib/firebase.ts: -// public getUserDocumentStarsPath(user: UserModelType, documentKey?: string, starKey?: string) { -// const docSuffix = documentKey ? `/${documentKey}` : ""; -// const starSuffix = starKey ? `/${starKey}` : ""; -// return `${this.getOfferingPath(user)}/commentaries/stars${docSuffix}${starSuffix}`; -// } diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 07833d8eb8..597fd7fa95 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -44,7 +44,7 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { onClick: () => setSortBy(option) })); - const filterItems: ICustomDropdownItem[] = filterOptions.map((option) => ({ + const docFilterOptions: ICustomDropdownItem[] = filterOptions.map((option) => ({ selected: option === docFilter, text: option, onClick: () => handleDocFilterSelection(option) @@ -80,8 +80,8 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { : <> diff --git a/src/components/document/sorted-documents.tsx b/src/components/document/sorted-documents.tsx index 3544a15ee7..898b819284 100644 --- a/src/components/document/sorted-documents.tsx +++ b/src/components/document/sorted-documents.tsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import { DocumentContextReact } from "./document-context"; import { SortedDocument } from "../../models/stores/sorted-documents"; -import { DocumentModelType, getDocumentContext, isDocumentModel } from "../../models/document/document"; +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"; @@ -49,21 +49,17 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen return downloadedDocs.length; }; - const getDocumentFromMetadata = (docKey: string) => { - return sortedDocuments.fetchFullDocument(docKey); - }; - const handleSelectDocument = async (document: DocumentModelType | IDocumentMetadata) => { + persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); try { - const doc = isDocumentModel(document) - ? getDocument(document.key) - : getDocument(document.key) || await getDocumentFromMetadata(document.key); - persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); - if (doc) { - logDocumentViewEvent(doc as DocumentModelType); + // The full document data is needed to log a view event, but we may only have the metadata (if type + // of `document` is `IDocumentMetadata`). Use `getDocument` to attempt get the full document data. + const fullDoc = getDocument(document.key); + if (fullDoc) { + logDocumentViewEvent(fullDoc); } } catch (e) { - console.error("Error selecting document", e); + console.warn("Error logging document view", e); } }; @@ -73,7 +69,7 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen const renderDocumentItem = (doc: any) => { const fullDocument = getDocument(doc.key); - if(docFilter === "Problem" && fullDocument) { + if (docFilter === "Problem" && fullDocument) { return = observer(function SortedDocumen problemOrdinal={doc.problem} onSelectDocument={handleSelectDocument} />; - } else { - return null; } }; diff --git a/src/components/navigation/sort-work-header.tsx b/src/components/navigation/sort-work-header.tsx index b87d18f120..fe2151e712 100644 --- a/src/components/navigation/sort-work-header.tsx +++ b/src/components/navigation/sort-work-header.tsx @@ -5,14 +5,14 @@ import { CustomSelect, ICustomDropdownItem } from "../../clue/components/custom- import "./sort-work-header.scss"; interface ISortHeaderProps{ - filter: string; - filterItems: ICustomDropdownItem[]; + docFilter: string; + docFilterItems: ICustomDropdownItem[]; primarySort: string; primarySortItems: ICustomDropdownItem[]; } export const SortWorkHeader:React.FC= observer(function SortWorkView(props){ - const { filter, filterItems, primarySort, primarySortItems } = props; + const { docFilter, docFilterItems, primarySort, primarySortItems } = props; return (
@@ -33,8 +33,8 @@ export const SortWorkHeader:React.FC= observer(function SortWo
diff --git a/src/models/document/document.ts b/src/models/document/document.ts index d279321857..0b93a7587d 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -317,10 +317,6 @@ export const DocumentModel = Tree.named("Document") export type DocumentModelType = Instance; export type DocumentModelSnapshotType = SnapshotIn; -export const isDocumentModel = (document: any): document is DocumentModelType => { - return DocumentModel.is(document); -}; - export const getDocumentContext = (document: DocumentModelType): IDocumentContext => { const { type, key, title, originDoc } = document; return { diff --git a/src/models/stores/documents.ts b/src/models/stores/documents.ts index eac20aab4d..80451fa8e9 100644 --- a/src/models/stores/documents.ts +++ b/src/models/stores/documents.ts @@ -14,6 +14,7 @@ import { DEBUG_DOCUMENT } from "../../lib/debug"; import { Firestore } from "../../lib/firestore"; import { TreeManagerType } from "../history/tree-manager"; import { UserContextProvider } from "./user-context-provider"; +import { typeConverter } from "../../utilities/db-utils"; const extractLatestPublications = (publications: DocumentModelType[], attr: "uid" | "originDoc") => { const latestPublications: DocumentModelType[] = []; @@ -52,15 +53,15 @@ export const DocumentsModel = types }, async fetchDocument(documentKey: string) { let doc = self.all.find((document) => document.key === documentKey); + if (!doc) { - const docSnapshot = await self.firestore?.collection("documents").where("key", "==", documentKey) + const converter = typeConverter(); + const docSnapshot = await self.firestore?.collection("documents") + .withConverter(converter) + .where("key", "==", documentKey) .where("context_id", "==", self.userContextProvider?.userContext.classHash).get(); if (docSnapshot?.docs.length) { - const _doc = docSnapshot?.docs[0].data(); - const key = _doc?.data().key; - const type = _doc?.data().type; - const uid = _doc?.data().uid; - doc = {..._doc.data(), key, type, uid}; + doc = docSnapshot?.docs[0].data(); } } return doc; diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 68de62d36b..7cb47469f5 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -70,14 +70,7 @@ export class SortedDocuments { get user() { return this.stores.user; } - - - // The error is occurring because the this.firestoreMetadataDocs array is of type - // IObservableArray, but the filteredDocsByType getter is returning - // an array of type IDocumentMetadata[]. To fix this, you can convert the IObservableArray - // to a regular array using the slice() method. get filteredDocsByType(): IDocumentMetadata[] { - // const documents = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; return this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => { return isSortableType(doc.type); }); @@ -178,22 +171,12 @@ export class SortedDocuments { // adding in (exemplar) documents with authored tags const allSortableDocKeys = this.filteredDocsByType; allSortableDocKeys.forEach(doc => { - // if (this.stores.docFilter === "Problem") { - // const foundTagKey = doc.getProperty("authoredCommentTag"); - // if (foundTagKey !== undefined && foundTagKey !== "") { - // if (tagsWithDocs[foundTagKey]) { - // tagsWithDocs[foundTagKey].docKeysFoundWithTag.push(doc.key); - // uniqueDocKeysWithTags.add(doc.key); - // } - // } - // } else { - doc.strategies?.forEach(strategy => { - if (tagsWithDocs[strategy]) { - tagsWithDocs[strategy].docKeysFoundWithTag.push(doc.key); - uniqueDocKeysWithTags.add(doc.key); - } - }); - //} + doc.strategies?.forEach(strategy => { + if (tagsWithDocs[strategy]) { + tagsWithDocs[strategy].docKeysFoundWithTag.push(doc.key); + uniqueDocKeysWithTags.add(doc.key); + } + }); }); allSortableDocKeys.forEach(doc => { @@ -210,7 +193,6 @@ export class SortedDocuments { const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; const sectionLabel = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; - // const docs = this.stores.docFilter === "Problem" ? this.documents.all : this.firestoreMetadataDocs; const documents = this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); sortedDocsArr.push({ sectionLabel, From 743c43a1038364064af06fa2ccf578a7d475b0f8 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 23 Jul 2024 18:14:12 -0400 Subject: [PATCH 006/127] chore: update jest test --- src/models/stores/sorted-documents.test.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 8e9f131695..8c0e2f7cac 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 "../../../functions/src/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, + tileTypes: [] + }, + { + uid: "2", //Scott + type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, + tileTypes: ["Text"] + }, + { + uid: "3", //Dennis + type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, + tileTypes: ["Drawing"] + }, + { + uid: "4", //Kirk + type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, + tileTypes: [] + } +]); + const createMockDocuments = () => { return mockDocumentsData.map(createDocumentModel); }; @@ -135,6 +160,7 @@ describe('Sorted Documents Model', () => { }; sortedDocuments = new SortedDocuments(mockStores as ISortedDocumentsStores); + sortedDocuments.firestoreMetadataDocs = mockMetadataDocuments; }); From 70dfff31f4445b6b79239d0b3fea3142c8448fbb Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 23 Jul 2024 18:25:30 -0400 Subject: [PATCH 007/127] chore: remove commented-out code, instances of `.only` --- cypress/e2e/functional/teacher_tests/teacher_share_spec.js | 2 +- .../e2e/functional/teacher_tests/teacher_sort_work_view_spec.js | 2 +- src/models/stores/sorted-documents.ts | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js index 25184ddfe8..84d40c39a1 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js @@ -39,7 +39,7 @@ context('Teacher Sharing', function() { verifySwitch('private'); }); - it('does not allow student to access private teacher document', function() { + it.only('does not allow student to access private teacher document', function() { cy.visit(studentQueryParams); cy.waitForLoad(); verifyStudentSeesAsPrivate(); 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 289c68e9bd..4cf969783c 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 @@ -66,7 +66,7 @@ describe('SortWorkView Tests', () => { sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed }); - it.only("should open Sort Work tab and test showing by Problem, Investigation, Unit, All", () => { + it("should open Sort Work tab and test showing by Problem, Investigation, Unit, All", () => { beforeTest(queryParams1); sortWork.getShowForMenu().should("be.visible"); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 7cb47469f5..9f509cb908 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -337,7 +337,6 @@ export class SortedDocuments { addDocByType(doc, "No Tools"); } } - //} }); // Map the tile types to their display names From aa85143084f37257d1cdfaa30013e603a41c9476 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 23 Jul 2024 20:37:08 -0400 Subject: [PATCH 008/127] chore: update cypress tests --- .../teacher_tests/teacher_share_spec.js | 2 +- .../teacher_sort_work_view_spec.js | 28 +++++++++++++++---- cypress/support/elements/common/SortedWork.js | 5 +--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js index 84d40c39a1..25184ddfe8 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js @@ -39,7 +39,7 @@ context('Teacher Sharing', function() { verifySwitch('private'); }); - it.only('does not allow student to access private teacher document', function() { + it('does not allow student to access private teacher document', function() { cy.visit(studentQueryParams); cy.waitForLoad(); verifyStudentSeesAsPrivate(); 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 4cf969783c..f7a774a299 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 @@ -14,8 +14,11 @@ let chatPanel = new ChatPanel; const canvas = new Canvas; const title = "1.1 Unit Toolbar Configuration"; const copyTitle = "Personal Workspace"; -const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`; +// TODO: Bring back queryParams1? For now we're using queryParams3 instead because it +// uses the TAGCLUE demo space which has updated metadata docs. +// const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`; const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`; +const queryParams3 = `${Cypress.config("tagClueqaConfigSubtabsUnitTeacher1")}`; function beforeTest(params) { cy.clearQAData('all'); @@ -43,7 +46,7 @@ function runClueAsStudent(student, group = 5) { describe('SortWorkView Tests', () => { it('should open SortWorkView tab and interact with it', () => { - beforeTest(queryParams1); + beforeTest(queryParams3); cy.log('verify clicking the sort menu'); sortWork.getSortByMenu().click(); // Open the sort menu cy.wait(1000); @@ -67,7 +70,7 @@ describe('SortWorkView Tests', () => { }); it("should open Sort Work tab and test showing by Problem, Investigation, Unit, All", () => { - beforeTest(queryParams1); + beforeTest(queryParams3); sortWork.getShowForMenu().should("be.visible"); sortWork.getShowForProblemOption().should("have.class", "selected"); // "Problem" selected by default @@ -76,16 +79,31 @@ describe('SortWorkView Tests', () => { sortWork.getShowForAllOption().should("exist"); cy.get(".section-header-arrow").click({multiple: true}); // Open the sections - // For the "Problem" option, the documents should be listed using the larger thumbnail view - // [data-test=sort-work-list-items] should have a length greater than 0 + // 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"); }); diff --git a/cypress/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index be2e7e20ad..b4ca1390d9 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -27,7 +27,7 @@ class SortedWork { return cy.get("[data-test=list-item-problem]"); } getShowForInvestigationOption() { - return cy.get("[data-test=list-item-problem]"); + return cy.get("[data-test=list-item-investigation]"); } getShowForUnitOption() { return cy.get("[data-test=list-item-unit]"); @@ -35,9 +35,6 @@ class SortedWork { getShowForAllOption() { return cy.get("[data-test=list-item-all]"); } - getShowForMenuOption(level) { - return cy.get("[data-test=filter-work-menu-list]").contains(level); - } 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}); } From 5a3f4b7ebcb464a8456c4d5a3b8b23e4c1aad220 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 24 Jul 2024 11:23:40 -0400 Subject: [PATCH 009/127] chore: code review suggestions --- src/components/document/sort-work-view.scss | 7 +++++ src/components/document/sort-work-view.tsx | 6 +--- src/components/document/sorted-documents.tsx | 22 ++++----------- src/models/document/log-document-event.ts | 26 ++++++++++++------ src/models/stores/sorted-documents.ts | 29 +------------------- 5 files changed, 32 insertions(+), 58 deletions(-) diff --git a/src/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index 16fd6bae7c..a1c02081f8 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -68,6 +68,13 @@ $title-margin: 2px; } } } + .loading-spinner { + background-image: url("../../assets/Spinner-1s-200px.svg"); + background-size: contain; + background-repeat: no-repeat; + width: 100px; + height: 75px; + } } .focus-document { diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 597fd7fa95..6bb00805a8 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -23,19 +23,15 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { const sortOptions = ["Group", "Name", sortTagPrompt, "Bookmarked", "Tools"]; const filterOptions: DocFilterType[] = ["Problem", "Investigation", "Unit", "All"]; const [sortBy, setSortBy] = useState("Group"); - const [docFilter, setDocFilter] = useState(persistentUIDocFilter); + const docFilter = persistentUIDocFilter; const handleDocFilterSelection = (filter: DocFilterType) => { sortedDocuments.setDocFilter(filter); persistentUI.setDocFilter(filter); - setDocFilter(filter); }; useEffect(()=>{ sortedDocuments.setDocFilter(docFilter); - if (sortBy === sortTagPrompt){ - sortedDocuments.updateTagDocumentMap(); - } sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); },[sortedDocuments, sortBy, sortTagPrompt, docFilter, investigation, unit, problem]); diff --git a/src/components/document/sorted-documents.tsx b/src/components/document/sorted-documents.tsx index 898b819284..58f3adb599 100644 --- a/src/components/document/sorted-documents.tsx +++ b/src/components/document/sorted-documents.tsx @@ -40,27 +40,13 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen }; const documentCount = () => { - if (docFilter !== "Problem") return sortedSection.documents.length; - - const downloadedDocs = sortedSection.documents.filter(doc => { - const exists = getDocument(doc.key); - return exists; - }); + const downloadedDocs = sortedSection.documents.filter(doc => getDocument(doc.key)); return downloadedDocs.length; }; const handleSelectDocument = async (document: DocumentModelType | IDocumentMetadata) => { persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); - try { - // The full document data is needed to log a view event, but we may only have the metadata (if type - // of `document` is `IDocumentMetadata`). Use `getDocument` to attempt get the full document data. - const fullDoc = getDocument(document.key); - if (fullDoc) { - logDocumentViewEvent(fullDoc); - } - } catch (e) { - console.warn("Error logging document view", e); - } + logDocumentViewEvent(document); }; const handleToggleShowDocuments = () => { @@ -78,7 +64,9 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen allowDelete={false} onSelectDocument={handleSelectDocument} />; - } else if (docFilter !== "Problem") { + } else if (docFilter === "Problem") { + return
; + } else { return { - 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/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 9f509cb908..16f9a3b86e 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -1,4 +1,4 @@ -import { ObservableSet, makeAutoObservable, runInAction, IObservableArray, observable } from "mobx"; +import { makeAutoObservable, runInAction, IObservableArray, observable } from "mobx"; import { isSortableType } from "../document/document-types"; import { DocumentsModelType } from "./documents"; import { GroupsModelType } from "./groups"; @@ -202,33 +202,6 @@ export class SortedDocuments { 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); - }); - } - }); - }); - }); - }); - } - async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { const db = this.db.firestore; const converter = typeConverter(); From a2397444f45864faa03d5da52b21245381e514b9 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 24 Jul 2024 13:02:15 -0400 Subject: [PATCH 010/127] chore: split `problemOrdinal` to separate `problem` and `investigation` --- src/lib/db.ts | 8 +++++--- src/models/document/document-utils.ts | 9 +++++---- src/models/document/document.ts | 3 ++- src/models/stores/sorted-documents.ts | 7 ++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index 3a832d1f26..c896cb65bb 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -87,7 +87,8 @@ export interface OpenDocumentOptions { groupUserConnections?: Record; originDoc?: string; pubVersion?: number; - problemOrdinal?: string; + problem?: string; + investigation?: string; unit?: string; } @@ -547,7 +548,7 @@ export class DB { public openDocument(options: OpenDocumentOptions) { const { documents } = this.stores; const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion, - problemOrdinal, unit} = options; + problem, investigation, unit} = options; return new Promise((resolve, reject) => { const {user} = this.stores; const documentPath = this.firebase.getUserDocumentPath(user, documentKey, userId); @@ -591,7 +592,8 @@ export class DB { content: content ? content : {}, changeCount: document.changeCount, pubVersion, - problemOrdinal, + problem, + investigation, unit }); } catch (e) { diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index ebb55c723e..adfb1ee6f6 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -12,10 +12,11 @@ export function getDocumentDisplayTitle( unit?: string ) { const { type } = document; - const problemTitle = !(document.problemOrdinal || document.unit) || - (document.problemOrdinal === String(problem?.ordinal) && unit === document?.unit) - ? problem?.title || "Unknown Problem" - : "Unknown Problem"; + 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) diff --git a/src/models/document/document.ts b/src/models/document/document.ts index 0b93a7587d..9cec2fe16c 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -55,7 +55,8 @@ export const DocumentModel = Tree.named("Document") changeCount: types.optional(types.number, 0), pubVersion: types.maybe(types.number), supportContentType: types.maybe(types.enumeration("SupportType", Object.values(ESupportType))), - problemOrdinal: types.maybe(types.string), + problem: types.maybe(types.string), + investigation: types.maybe(types.string), unit: types.maybe(types.string), }) .volatile(self => ({ diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 16f9a3b86e..9f79df59c9 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -243,11 +243,7 @@ export class SortedDocuments { const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); if (!metadataDoc) return; - const problemOrdinal = metadataDoc?.type === "problem" - ? `${metadataDoc?.investigation}.${metadataDoc?.problem}` - : undefined; const unit = metadataDoc?.type === "problem" ? metadataDoc?.unit : undefined; - const props = { documentKey: metadataDoc?.key, type: metadataDoc?.type as any, @@ -258,7 +254,8 @@ export class SortedDocuments { visibility: undefined, originDoc: undefined, pubVersion: undefined, - problemOrdinal, + problem: metadataDoc?.problem, + investigation: metadataDoc?.investigation, unit, }; From adf223b7ac603a7000c7c48036103645684c3e11 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 24 Jul 2024 17:07:26 -0400 Subject: [PATCH 011/127] chore: code review refactor suggestions --- src/components/document/sort-work-view.tsx | 4 +- src/lib/db.ts | 9 ++++- src/models/stores/sorted-documents.test.ts | 1 - src/models/stores/sorted-documents.ts | 44 ++++++++++------------ src/models/stores/stores.ts | 1 - 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 6bb00805a8..c8934e9622 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -26,14 +26,12 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { const docFilter = persistentUIDocFilter; const handleDocFilterSelection = (filter: DocFilterType) => { - sortedDocuments.setDocFilter(filter); persistentUI.setDocFilter(filter); }; useEffect(()=>{ - sortedDocuments.setDocFilter(docFilter); sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); - },[sortedDocuments, sortBy, sortTagPrompt, docFilter, investigation, unit, problem]); + }, [docFilter, unit.code, investigation.ordinal, problem.ordinal, sortedDocuments]); const sortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ text: option, diff --git a/src/lib/db.ts b/src/lib/db.ts index c896cb65bb..131306d0a6 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -100,6 +100,7 @@ export class DB { public stores: IStores; private authStateUnsubscribe?: firebase.Unsubscribe; + private documentFetchPromiseMap = new Map>(); constructor() { makeObservable(this); @@ -549,7 +550,10 @@ export class DB { const { documents } = this.stores; const {documentKey, type, title, properties, userId, groupId, visibility, originDoc, pubVersion, problem, investigation, unit} = options; - return new Promise((resolve, reject) => { + 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); @@ -624,6 +628,9 @@ export class DB { reject(msg); }); }); + + this.documentFetchPromiseMap.set(documentKey, documentFetchPromise); + return documentFetchPromise; } public createLearningLogDocument(title?: string) { diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 8c0e2f7cac..99e6045f9a 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -156,7 +156,6 @@ describe('Sorted Documents Model', () => { documents: { all: mockDocuments }, groups: mockGroups, class: mockClass, - docFilter: "Problem" }; sortedDocuments = new SortedDocuments(mockStores as ISortedDocumentsStores); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 9f79df59c9..dbef71cd9d 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -9,7 +9,6 @@ import { Bookmarks } from "./bookmarks"; import { UserModelType } from "./user"; import { getTileContentInfo } from "../tiles/tile-content-info"; import { getTileComponentInfo } from "../tiles/tile-component-info"; -import { DocFilterType } from "./ui-types"; import { IDocumentMetadata } from "../../../functions/src/shared"; import { typeConverter } from "../../utilities/db-utils"; @@ -34,7 +33,6 @@ export interface ISortedDocumentsStores { db: DB; appConfig: AppConfigModelType; bookmarks: Bookmarks; - docFilter: DocFilterType; user: UserModelType; } @@ -240,26 +238,26 @@ export class SortedDocuments { } async fetchFullDocument(docKey: string) { - const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); - if (!metadataDoc) return; - - const unit = metadataDoc?.type === "problem" ? metadataDoc?.unit : undefined; - const props = { - documentKey: metadataDoc?.key, - type: metadataDoc?.type as any, - title: metadataDoc?.title || undefined, - properties: metadataDoc?.properties, - userId: metadataDoc?.uid, - groupId: undefined, - visibility: undefined, - originDoc: undefined, - pubVersion: undefined, - problem: metadataDoc?.problem, - investigation: metadataDoc?.investigation, - unit, - }; + const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); + if (!metadataDoc) return; + + const unit = metadataDoc?.unit ?? undefined; + const props = { + documentKey: metadataDoc?.key, + type: metadataDoc?.type as any, + title: metadataDoc?.title || undefined, + properties: metadataDoc?.properties, + userId: metadataDoc?.uid, + groupId: undefined, + visibility: undefined, + originDoc: undefined, + pubVersion: undefined, + problem: metadataDoc?.problem, + investigation: metadataDoc?.investigation, + unit, + }; - return this.db.openDocument(props); + return this.db.openDocument(props); } //*************************************** Sort By Bookmarks ************************************* @@ -336,8 +334,4 @@ export class SortedDocuments { return sortedByLabel; } - setDocFilter(filter: DocFilterType) { - this.stores.docFilter = filter; - } - } diff --git a/src/models/stores/stores.ts b/src/models/stores/stores.ts index fa4422dde6..d1da65702f 100644 --- a/src/models/stores/stores.ts +++ b/src/models/stores/stores.ts @@ -80,7 +80,6 @@ class Stores implements IStores{ ui: UIModelType; groups: GroupsModelType; class: ClassModelType; - docFilter: "Problem" | "Investigation" | "Unit" = "Problem"; documents: DocumentsModelType; networkDocuments: DocumentsModelType; db: DB; From a499e748da29ef2f8e4914da87bbe1fe3db8d8a6 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 24 Jul 2024 17:29:44 -0400 Subject: [PATCH 012/127] chore: disable tests that won't work until all metadata docs are updated --- .../teacher_tests/teacher_sort_work_view_spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 f7a774a299..b0a509d94a 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 @@ -14,11 +14,8 @@ let chatPanel = new ChatPanel; const canvas = new Canvas; const title = "1.1 Unit Toolbar Configuration"; const copyTitle = "Personal Workspace"; -// TODO: Bring back queryParams1? For now we're using queryParams3 instead because it -// uses the TAGCLUE demo space which has updated metadata docs. -// const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`; +const queryParams1 = `${Cypress.config("clueTestqaConfigSubtabsUnitTeacher6")}`; const queryParams2 = `${Cypress.config("qaConfigSubtabsUnitTeacher1")}`; -const queryParams3 = `${Cypress.config("tagClueqaConfigSubtabsUnitTeacher1")}`; function beforeTest(params) { cy.clearQAData('all'); @@ -46,7 +43,7 @@ function runClueAsStudent(student, group = 5) { describe('SortWorkView Tests', () => { it('should open SortWorkView tab and interact with it', () => { - beforeTest(queryParams3); + beforeTest(queryParams1); cy.log('verify clicking the sort menu'); sortWork.getSortByMenu().click(); // Open the sort menu cy.wait(1000); @@ -70,7 +67,7 @@ describe('SortWorkView Tests', () => { }); it("should open Sort Work tab and test showing by Problem, Investigation, Unit, All", () => { - beforeTest(queryParams3); + beforeTest(queryParams1); sortWork.getShowForMenu().should("be.visible"); sortWork.getShowForProblemOption().should("have.class", "selected"); // "Problem" selected by default @@ -106,8 +103,11 @@ describe('SortWorkView Tests', () => { sortWork.getFocusDocument().should("be.visible"); }); - - it("should open Sort Work tab and test sorting by group", () => { + // TODO: Reinstate the tests below. They will fail until all metadata documents are updated with + // the new fields. Currently, we've only updated a small set of metadata documents in some of the + // demo spaces. (The tests above use the CLUE-Test demo which has updated metadata documents, so + // they should work.) + it.skip("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'); From c508f881b35a476d441aff63df92f4d46c2c427f Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 24 Jul 2024 22:21:49 -0400 Subject: [PATCH 013/127] chore: disable more tests --- cypress/e2e/functional/document_tests/exemplar_test_spec.js | 3 ++- cypress/e2e/functional/teacher_tests/teacher_share_spec.js | 6 ++++-- .../functional/teacher_tests/teacher_sort_work_view_spec.js | 5 +---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/functional/document_tests/exemplar_test_spec.js b/cypress/e2e/functional/document_tests/exemplar_test_spec.js index 12039bdddb..a68a3bfb31 100644 --- a/cypress/e2e/functional/document_tests/exemplar_test_spec.js +++ b/cypress/e2e/functional/document_tests/exemplar_test_spec.js @@ -34,7 +34,8 @@ function addText(x, y, text) { drawToolTile.getTextDrawing().get('textarea').type(text + "{enter}"); } -context('Exemplar Documents', function () { + // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. + context.skip('Exemplar Documents', function () { it('Unit with default config does not reveal exemplars or generate sticky notes', function () { beforeTest(queryParams2); cy.openTopTab('sort-work'); diff --git a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js index 25184ddfe8..a698e22df1 100644 --- a/cypress/e2e/functional/teacher_tests/teacher_share_spec.js +++ b/cypress/e2e/functional/teacher_tests/teacher_share_spec.js @@ -39,7 +39,8 @@ context('Teacher Sharing', function() { verifySwitch('private'); }); - it('does not allow student to access private teacher document', function() { + // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. + it.skip('does not allow student to access private teacher document', function() { cy.visit(studentQueryParams); cy.waitForLoad(); verifyStudentSeesAsPrivate(); @@ -52,7 +53,8 @@ context('Teacher Sharing', function() { verifySwitch('public'); }); - it('allows student to access public teacher document', function() { + // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. + it.skip('allows student to access public teacher document', function() { cy.visit(studentQueryParams); cy.waitForLoad(); verifyStudentSeesAsPublic(); 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 b0a509d94a..7c44492d4c 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 @@ -103,10 +103,7 @@ describe('SortWorkView Tests', () => { sortWork.getFocusDocument().should("be.visible"); }); - // TODO: Reinstate the tests below. They will fail until all metadata documents are updated with - // the new fields. Currently, we've only updated a small set of metadata documents in some of the - // demo spaces. (The tests above use the CLUE-Test demo which has updated metadata documents, so - // they should work.) + // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. it.skip("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'); From e801cda80f16ce3a58a3faebb5cf4e6f0c5854a9 Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 25 Jul 2024 15:16:59 -0400 Subject: [PATCH 014/127] Checkpoint: modeling for subsorting. --- src/components/document/sort-work-view.tsx | 41 +- .../navigation/sort-work-header.tsx | 13 +- .../sorted-documents-documents-group.ts | 120 ++++++ src/models/stores/sorted-documents.test.ts | 108 ++--- src/models/stores/sorted-documents.ts | 372 +++++++----------- src/utilities/sort-document-utils.ts | 172 ++++++++ 6 files changed, 520 insertions(+), 306 deletions(-) create mode 100644 src/models/stores/sorted-documents-documents-group.ts create mode 100644 src/utilities/sort-document-utils.ts diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index c8934e9622..4a38fe3bb4 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -22,7 +22,8 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { const sortTagPrompt = tagPrompt || ""; //first dropdown choice for comment tags const sortOptions = ["Group", "Name", sortTagPrompt, "Bookmarked", "Tools"]; const filterOptions: DocFilterType[] = ["Problem", "Investigation", "Unit", "All"]; - const [sortBy, setSortBy] = useState("Group"); + const [primarySortBy, setPrimarySortBy] = useState("Group"); + const [secondarySortBy, setSecondarySortBy] = useState("Name"); const docFilter = persistentUIDocFilter; const handleDocFilterSelection = (filter: DocFilterType) => { @@ -33,9 +34,14 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); }, [docFilter, unit.code, investigation.ordinal, problem.ordinal, sortedDocuments]); - const sortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + const primarySortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ text: option, - onClick: () => setSortBy(option) + onClick: () => setPrimarySortBy(option) + })); + + const secondarySortOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + text: option, + onClick: () => setSecondarySortBy(option) })); const docFilterOptions: ICustomDropdownItem[] = filterOptions.map((option) => ({ @@ -44,24 +50,9 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { onClick: () => handleDocFilterSelection(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 primarySearchTerm = primarySortBy === sortTagPrompt ? "Strategy" : primarySortBy; + const secondarySearchTerm = secondarySortBy === sortTagPrompt ? "Strategy" : secondarySortBy; + const renderedSortedDocuments = sortedDocuments.sortDocuments(primarySearchTerm, secondarySearchTerm); const tabState = persistentUI.tabs.get(ENavTab.kSortWork); const openDocumentKey = tabState?.openDocuments.get(ENavTab.kSortWork) || ""; @@ -76,10 +67,12 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { -
+
{ renderedSortedDocuments && renderedSortedDocuments.map((sortedSection, idx) => { return ( diff --git a/src/components/navigation/sort-work-header.tsx b/src/components/navigation/sort-work-header.tsx index fe2151e712..7b8859b168 100644 --- a/src/components/navigation/sort-work-header.tsx +++ b/src/components/navigation/sort-work-header.tsx @@ -9,10 +9,12 @@ interface ISortHeaderProps{ docFilterItems: ICustomDropdownItem[]; primarySort: string; primarySortItems: ICustomDropdownItem[]; + secondarySort: string; + secondarySortItems: ICustomDropdownItem[]; } export const SortWorkHeader:React.FC= observer(function SortWorkView(props){ - const { docFilter, docFilterItems, primarySort, primarySortItems } = props; + const { docFilter, docFilterItems, primarySort, primarySortItems, secondarySort, secondarySortItems } = props; return (
@@ -26,6 +28,15 @@ export const SortWorkHeader:React.FC= observer(function SortWo showItemChecks={true} />
+
+ +
Show for
diff --git a/src/models/stores/sorted-documents-documents-group.ts b/src/models/stores/sorted-documents-documents-group.ts new file mode 100644 index 0000000000..dfaef77e4f --- /dev/null +++ b/src/models/stores/sorted-documents-documents-group.ts @@ -0,0 +1,120 @@ +import { IDocumentMetadata } from "functions/src/shared"; +import { ISortedDocumentsStores, TagWithDocs } from "./sorted-documents"; +import { makeAutoObservable } from "mobx"; +import { + createDocMapByBookmarks, + createDocMapByGroups, + createDocMapByNames, + createTileTypeToDocumentsMap, + getTagsWithDocs, + SortedDocument, + sortGroupSectionLabels, + sortNameSectionLabels +} from "../../utilities/sort-document-utils"; +import { getTileContentInfo } from "../tiles/tile-content-info"; +import { getTileComponentInfo } from "../tiles/tile-component-info"; + +import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; + +export class DocumentGroup { + stores: ISortedDocumentsStores; + value: string; + metaDataDocs: IDocumentMetadata[]; + sortCategory: string; // "Group", "Name", "Strategy", "Bookmark", "Tool" + firestoreTagDocumentMap = new Map>(); + + constructor(stores: ISortedDocumentsStores, value: string, metaDataDocs: IDocumentMetadata[], sortCategory: string) { + makeAutoObservable(this); + this.stores = stores; + this.value = value; + this.metaDataDocs = metaDataDocs; + this.sortCategory = sortCategory; + } + + get all(): SortedDocument[] { + return [{ + sectionLabel: this.value, + documents: this.metaDataDocs + }]; + } + + get groups(): SortedDocument[] { + const documentMap = createDocMapByGroups(this.metaDataDocs, this.stores.groups.groupForUser); + const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map(sectionLabel => { + return { + sectionLabel, + documents: documentMap.get(sectionLabel)!.documents + }; + }); + } + + get names(): SortedDocument[] { + const documentMap = createDocMapByNames(this.metaDataDocs, this.stores.class.getUserById); + const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map((sectionLabel) =>{ + return { + sectionLabel, + documents: documentMap.get(sectionLabel).documents + }; + }); + } + + get strategies(): SortedDocument[] { + const commentTags = this.stores.appConfig.commentTags; + const tagsWithDocs = getTagsWithDocs(this.metaDataDocs, commentTags, this.firestoreTagDocumentMap); + + const sortedDocsArr: SortedDocument[] = []; + Object.entries(tagsWithDocs).forEach((tagKeyAndValObj) => { + const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; + const sectionLabel = tagWithDocs.tagValue; + const docKeys = tagWithDocs.docKeysFoundWithTag; + const documents = this.metaDataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); + sortedDocsArr.push({ + sectionLabel, + documents + }); + }); + return sortedDocsArr; + } + + get tools(): SortedDocument[] { + const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.metaDataDocs); + + // 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; + } + 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); + }); + + return sortedByLabel; + } + + get bookmarks(): SortedDocument[] { + const documentMap = createDocMapByBookmarks(this.metaDataDocs, this.stores.bookmarks); + const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; + return sortedSectionLabels.filter(label => documentMap.has(label)) + .map(label => ({ + sectionLabel: label, + documents: documentMap.get(label).documents + })); + } +} diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 99e6045f9a..6162e09012 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -163,58 +163,58 @@ describe('Sorted Documents Model', () => { }); - describe('sortByGroup Function', () => { - it('should correctly sort documents by group', () => { - const sortedDocsByGroup = sortedDocuments.sortByGroup; - expect(sortedDocsByGroup.length).toBe(3); - const group3 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 3'); - expect(group3?.documents.length).toBe(2); // Group 3 - Kirk + Scott - const group5 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 5'); - expect(group5?.documents.length).toBe(1); // Group 5 - Joe - const group9 = sortedDocsByGroup.find(group => group.sectionLabel === '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); - expect(sortedSectionLabels).toEqual(['Group 3', 'Group 5', 'Group 9']); - }); - }); - - describe('sortByName Function', () => { - it('should correctly sort documents by last name', () => { - const expectedOrder = [ - "Bacal, Joe", - "Cao, Dennis", - "Cytacki, Scott", - "Swenson, Kirk" - ]; - const sortedDocsByName = sortedDocuments.sortByName; - const actualOrder = sortedDocsByName.map(group => group.sectionLabel); - expect(actualOrder).toEqual(expectedOrder); - }); - }); - - describe('sortByTools Function', () => { - it('should correctly sort documents by tool', () => { - const sortedDocsByTools = sortedDocuments.sortByTools; - const summaryOfResult = sortedDocsByTools.map(section => ({ - sectionLabel: section.sectionLabel, - docKeys: section.documents.map(doc => doc.key) - })); - expect(summaryOfResult).toEqual([ - { sectionLabel: "Sketch", docKeys: [ - "Student 3 Problem Doc Group 9" - ]}, - { sectionLabel: "Text", docKeys: [ - "Student 2 Problem Doc Group 3" - ]}, - { sectionLabel: "No Tools", docKeys: [ - "Student 1 Problem Doc Group 5", - "Student 4 Problem Doc Group 3" - ]} - ]); - }); - }); + // describe('sortByGroup Function', () => { + // it('should correctly sort documents by group', () => { + // const sortedDocsByGroup = sortedDocuments.sortByGroup; + // expect(sortedDocsByGroup.length).toBe(3); + // const group3 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 3'); + // expect(group3?.documents.length).toBe(2); // Group 3 - Kirk + Scott + // const group5 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 5'); + // expect(group5?.documents.length).toBe(1); // Group 5 - Joe + // const group9 = sortedDocsByGroup.find(group => group.sectionLabel === '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); + // expect(sortedSectionLabels).toEqual(['Group 3', 'Group 5', 'Group 9']); + // }); + // }); + + // describe('sortByName Function', () => { + // it('should correctly sort documents by last name', () => { + // const expectedOrder = [ + // "Bacal, Joe", + // "Cao, Dennis", + // "Cytacki, Scott", + // "Swenson, Kirk" + // ]; + // const sortedDocsByName = sortedDocuments.sortByName; + // const actualOrder = sortedDocsByName.map(group => group.sectionLabel); + // expect(actualOrder).toEqual(expectedOrder); + // }); + // }); + + // describe('sortByTools Function', () => { + // it('should correctly sort documents by tool', () => { + // const sortedDocsByTools = sortedDocuments.sortByTools; + // const summaryOfResult = sortedDocsByTools.map(section => ({ + // sectionLabel: section.sectionLabel, + // docKeys: section.documents.map(doc => doc.key) + // })); + // expect(summaryOfResult).toEqual([ + // { sectionLabel: "Sketch", docKeys: [ + // "Student 3 Problem Doc Group 9" + // ]}, + // { sectionLabel: "Text", docKeys: [ + // "Student 2 Problem Doc Group 3" + // ]}, + // { sectionLabel: "No Tools", docKeys: [ + // "Student 1 Problem Doc Group 5", + // "Student 4 Problem Doc Group 3" + // ]} + // ]); + // }); + // }); }); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index dbef71cd9d..48c4b12f14 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -8,19 +8,24 @@ import { AppConfigModelType } from "./app-config-model"; import { Bookmarks } from "./bookmarks"; import { UserModelType } from "./user"; import { getTileContentInfo } from "../tiles/tile-content-info"; -import { getTileComponentInfo } from "../tiles/tile-component-info"; import { IDocumentMetadata } from "../../../functions/src/shared"; import { typeConverter } from "../../utilities/db-utils"; - -import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; - -export type SortedDocument = { - sectionLabel: string; - documents: IDocumentMetadata[]; - icon?: React.FC>; //exists only in the "sort by tools" case -} - -type TagWithDocs = { +import { + createDocMapByBookmarks, + createDocMapByGroups, + createDocMapByNames, + createTileTypeToDocumentsMap, + getTagsWithDocs, + SortedDocument, + sortGroupSectionLabels, + sortNameSectionLabels +} from "../../utilities/sort-document-utils"; +import { DocumentGroup } from "./sorted-documents-documents-group"; + + +export type SortedDocumentsMap = Record; + +export type TagWithDocs = { tagKey: string; tagValue: string; docKeysFoundWithTag: string[]; @@ -46,160 +51,148 @@ export class SortedDocuments { 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 db(): DB { - return this.stores.db; - } get commentTags(): Record | undefined { return this.stores.appConfig.commentTags; } - get bookmarks() { - return this.stores.bookmarks; + get db(): DB { + return this.stores.db; } - get user() { - return this.stores.user; + get documents(): DocumentsModelType { + return this.stores.documents; } get filteredDocsByType(): IDocumentMetadata[] { return this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => { return isSortableType(doc.type); }); } - - //******************************************* 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; - }); - return sortedSectionLabels.map(sectionLabel => documentMap.get(sectionLabel)); + get groupsStore(): GroupsModelType { + return this.stores.groups; + } + get user() { + return this.stores.user; } - //******************************************* 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); + // ** views ** // + get groups(): DocumentGroup[] { + const documentMap = createDocMapByGroups(this.filteredDocsByType, this.groupsStore.groupForUser); + const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map(sectionLabel => { + return new DocumentGroup(this.stores, sectionLabel, documentMap.get(sectionLabel), "group"); }); - - 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); + } + get names(): DocumentGroup[] { + const documentMap = createDocMapByNames(this.filteredDocsByType, this.class.getUserById); + const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); + return sortedSectionLabels.map((sectionLabel) =>{ + return new DocumentGroup(this.stores, sectionLabel, documentMap.get(sectionLabel).documents, "name"); }); - return sortedSectionLabels.map(sectionLabel => documentMap.get(sectionLabel)); } - //*************************************** Sort By Strategy ************************************** - - get sortByStrategy(): SortedDocument[]{ + get strategies(): 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(); + const tagsWithDocs = getTagsWithDocs(this.firestoreMetadataDocs, commentTags, this.firestoreTagDocumentMap); - // 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 => { - doc.strategies?.forEach(strategy => { - if (tagsWithDocs[strategy]) { - tagsWithDocs[strategy].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 sortedDocsArr: SortedDocument[] = []; + const sortedDocsArr: DocumentGroup[] = []; Object.entries(tagsWithDocs).forEach((tagKeyAndValObj) => { const tagWithDocs = tagKeyAndValObj[1] as TagWithDocs; const sectionLabel = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; const documents = this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); - sortedDocsArr.push({ - sectionLabel, - documents - }); + sortedDocsArr.push(new DocumentGroup(this.stores, sectionLabel, documents, "strategy")); }); return sortedDocsArr; } + get tools(): DocumentGroup[] { + const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.firestoreMetadataDocs); + + const sectionedDocuments = Object.keys(tileTypeToDocumentsMap).map(tileType => { + const contentInfo = getTileContentInfo(tileType); + const value = contentInfo?.displayName || tileType; + const section: DocumentGroup = new DocumentGroup(this.stores, value, tileTypeToDocumentsMap[tileType], "tool"); + return section; + }); + + // Sort the tile types. 'No Tools' should be at the end. + const sortedByLabel = sectionedDocuments.sort((a, b) => { + if (a.value === "No Tools") return 1; // Move 'No Tools' to the end + if (b.value === "No Tools") return -1; // Alphabetically sort all others + return a.value.localeCompare(b.value); + }); + + return sortedByLabel; + } + + get bookmarks(): DocumentGroup[] { + const documentMap = createDocMapByBookmarks(this.firestoreMetadataDocs, this.bookmarksStore); + + const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; + return sortedSectionLabels.filter(label => documentMap.has(label)) + .map(label => new DocumentGroup(this.stores, + label, documentMap.get(label).documents, "bookmark")); + } + + sortDocuments (primarySort: string, secondarySort: string) { + let documentGroups: DocumentGroup[] = []; + switch (primarySort) { + case "Group": + documentGroups = this.groups; + break; + case "Name": + documentGroups = this.names; + break; + case "Strategy": + documentGroups = this.strategies; + break; + case "Tool": + documentGroups = this.tools; + break; + case "Bookmark": + documentGroups = this.bookmarks; + break; + } + const sortedDocuments: SortedDocumentsMap = {}; + switch (secondarySort) { + case "None": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.all; + }); + break; + case "Group": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.groups; + }); + break; + case "Name": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.names; + }); + break; + case "Strategy": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.strategies; + }); + break; + case "Tool": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.tools; + }); + break; + case "Bookmark": + documentGroups.forEach(documentGroup => { + sortedDocuments[documentGroup.value] = documentGroup.bookmarks; + }); + break; + } + return sortedDocuments; + } + async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { const db = this.db.firestore; const converter = typeConverter(); @@ -238,100 +231,25 @@ export class SortedDocuments { } async fetchFullDocument(docKey: string) { - const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); - if (!metadataDoc) return; - - const unit = metadataDoc?.unit ?? undefined; - const props = { - documentKey: metadataDoc?.key, - type: metadataDoc?.type as any, - title: metadataDoc?.title || undefined, - properties: metadataDoc?.properties, - userId: metadataDoc?.uid, - groupId: undefined, - visibility: undefined, - originDoc: undefined, - pubVersion: undefined, - problem: metadataDoc?.problem, - investigation: metadataDoc?.investigation, - unit, - }; - - return this.db.openDocument(props); - } - - //*************************************** 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 sortByTools(): SortedDocument[] { - const tileTypeToDocumentsMap: Record = {}; - - const addDocByType = (docToAdd: IDocumentMetadata, type: string) => { - if (!tileTypeToDocumentsMap[type]) { - tileTypeToDocumentsMap[type] = []; - } - tileTypeToDocumentsMap[type].push(docToAdd); + const metadataDoc = this.firestoreMetadataDocs.find(doc => doc.key === docKey); + if (!metadataDoc) return; + + const unit = metadataDoc?.unit ?? undefined; + const props = { + documentKey: metadataDoc?.key, + type: metadataDoc?.type as any, + title: metadataDoc?.title || undefined, + properties: metadataDoc?.properties, + userId: metadataDoc?.uid, + groupId: undefined, + visibility: undefined, + originDoc: undefined, + pubVersion: undefined, + problem: metadataDoc?.problem, + investigation: metadataDoc?.investigation, + unit, }; - //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) => { - if (doc.tileTypes) { - const validTileTypes = doc.tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); - if (validTileTypes.length > 0) { - validTileTypes.forEach(tileType => { - addDocByType(doc, tileType); - }); - // TODO: Sparrow annotations. We'll first need to add information about these to metadata docs. - } else { - 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; - } - 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); - }); - - return sortedByLabel; + return this.db.openDocument(props); } - } diff --git a/src/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts new file mode 100644 index 0000000000..b8393f2364 --- /dev/null +++ b/src/utilities/sort-document-utils.ts @@ -0,0 +1,172 @@ +import { IDocumentMetadata } from "functions/src/shared"; +import { Bookmarks } from "src/models/stores/bookmarks"; + +export type SortedDocument = { + sectionLabel: 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 = 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, { + sectionLabel, + documents: [] + }); + } + documentMap.get(sectionLabel).documents.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 = new Map(); + documents.forEach((doc) => { + const user = 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); + }); + 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 tileTypeToDocumentsMap: Record = {}; + + const addDocByType = (docToAdd: IDocumentMetadata, 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 + documents.forEach((doc) => { + if (doc.tileTypes) { + const validTileTypes = doc.tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); + if (validTileTypes.length > 0) { + validTileTypes.forEach(tileType => { + addDocByType(doc, tileType); + }); + // TODO: Sparrow annotations. We'll first need to add information about these to metadata docs. + } else { + addDocByType(doc, "No Tools"); + } + } + }); + + return tileTypeToDocumentsMap; +}; + +export const createDocMapByBookmarks = (documents: IDocumentMetadata[], bookmarks: Bookmarks) => { + const documentMap = new Map(); + documents.forEach((doc) => { + const sectionLabel = bookmarks.isDocumentBookmarked(doc.key) ? "Bookmarked" : "Not Bookmarked"; + if (!documentMap.has(sectionLabel)) { + documentMap.set(sectionLabel, { + sectionLabel, + documents: [] + }); + } + documentMap.get(sectionLabel).documents.push(doc); + }); + return documentMap; +}; From c629e9ce9a7a3688d5d935b9efd2973e8e37a728 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 29 Jul 2024 15:40:51 -0400 Subject: [PATCH 015/127] checkpoint: connect UI to modeling --- src/components/document/sort-work-view.scss | 65 ----------- src/components/document/sort-work-view.tsx | 50 ++++++--- src/components/document/sorted-section.scss | 73 ++++++++++++ ...orted-documents.tsx => sorted-section.tsx} | 72 ++++++++---- .../thumbnail/simple-document-item.tsx | 8 +- .../sorted-documents-documents-group.ts | 80 ++++++------- src/models/stores/sorted-documents.ts | 105 +++++------------- src/models/stores/ui-types.ts | 2 + src/utilities/sort-document-utils.ts | 28 +++-- 9 files changed, 253 insertions(+), 230 deletions(-) create mode 100644 src/components/document/sorted-section.scss rename src/components/document/{sorted-documents.tsx => sorted-section.tsx} (56%) diff --git a/src/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index a1c02081f8..eeca42f058 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -12,71 +12,6 @@ $title-margin: 2px; border-top: none; } - .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); - } - } - } - } - } - .loading-spinner { - background-image: url("../../assets/Spinner-1s-200px.svg"); - background-size: contain; - background-repeat: no-repeat; - width: 100px; - height: 75px; - } - } - .focus-document { display: flex; flex-direction: column; diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 4a38fe3bb4..6114da8a58 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -7,10 +7,13 @@ 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 { DocFilterType } from "../../models/stores/ui-types"; -import { SortedDocuments } from "./sorted-documents"; +import { DocFilterType, PrimarySortType, SecondarySortType } from "../../models/stores/ui-types"; +import { SortedSection } from "./sorted-section"; +import { DocumentGroup } from "../../models/stores/sorted-documents-documents-group"; + import "../thumbnail/document-type-collection.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. @@ -23,23 +26,25 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { const sortOptions = ["Group", "Name", sortTagPrompt, "Bookmarked", "Tools"]; const filterOptions: DocFilterType[] = ["Problem", "Investigation", "Unit", "All"]; const [primarySortBy, setPrimarySortBy] = useState("Group"); - const [secondarySortBy, setSecondarySortBy] = useState("Name"); + const [secondarySortBy, setSecondarySortBy] = useState("None"); const docFilter = persistentUIDocFilter; const handleDocFilterSelection = (filter: DocFilterType) => { persistentUI.setDocFilter(filter); }; - useEffect(()=>{ - sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); - }, [docFilter, unit.code, investigation.ordinal, problem.ordinal, sortedDocuments]); - const primarySortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + disabled: false, + selected: option === primarySortBy, text: option, onClick: () => setPrimarySortBy(option) })); - const secondarySortOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ + const secondarySortOptions: ICustomDropdownItem[] = []; + secondarySortOptions.push({ text: "None", onClick: () => setSecondarySortBy("None") }); + sortOptions.map((option) => secondarySortOptions.push({ + disabled: option === primarySortBy, + selected: option === secondarySortBy, text: option, onClick: () => setSecondarySortBy(option) })); @@ -50,14 +55,26 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { onClick: () => handleDocFilterSelection(option) })); - const primarySearchTerm = primarySortBy === sortTagPrompt ? "Strategy" : primarySortBy; - const secondarySearchTerm = secondarySortBy === sortTagPrompt ? "Strategy" : secondarySortBy; - const renderedSortedDocuments = sortedDocuments.sortDocuments(primarySearchTerm, secondarySearchTerm); + const primarySearchTerm = primarySortBy === sortTagPrompt ? "byStrategy" : `by${primarySortBy}` as PrimarySortType; + const secondarySearchTerm = secondarySortBy === sortTagPrompt + ? "byStrategy" + : `by${secondarySortBy}` as SecondarySortType; + const sortedDocumentGroups = sortedDocuments[primarySearchTerm]; 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]); + + useEffect(() => { + if (primarySortBy === secondarySortBy) { + setSecondarySortBy("None"); + } + }, [primarySortBy, secondarySortBy]); + return (
{ @@ -73,14 +90,15 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { secondarySortItems={secondarySortOptions} />
- { renderedSortedDocuments && - renderedSortedDocuments.map((sortedSection, idx) => { + { sortedDocumentGroups && + sortedDocumentGroups.map((documentGroup: DocumentGroup, idx: number) => { return ( - ); }) diff --git a/src/components/document/sorted-section.scss b/src/components/document/sorted-section.scss new file mode 100644 index 0000000000..fbaf4588ad --- /dev/null +++ b/src/components/document/sorted-section.scss @@ -0,0 +1,73 @@ +@import "../vars"; + +.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); + } + } + } + } + } + .loading-spinner { + background-image: url("../../assets/Spinner-1s-200px.svg"); + background-size: contain; + background-repeat: no-repeat; + width: 100px; + height: 75px; + } + .list { + + .doc-group { + display: flex; + width: 100%; + } + } +} diff --git a/src/components/document/sorted-documents.tsx b/src/components/document/sorted-section.tsx similarity index 56% rename from src/components/document/sorted-documents.tsx rename to src/components/document/sorted-section.tsx index 58f3adb599..8d95739c85 100644 --- a/src/components/document/sorted-documents.tsx +++ b/src/components/document/sorted-section.tsx @@ -2,29 +2,31 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import classNames from "classnames"; -import { DocumentContextReact } from "./document-context"; -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 { DocFilterType } from "../../models/stores/ui-types"; +import { DocFilterType, SecondarySortType } from "../../models/stores/ui-types"; import { SimpleDocumentItem } from "../thumbnail/simple-document-item"; import { IDocumentMetadata } from "../../../functions/src/shared"; +import { DocumentContextReact } from "./document-context"; +import { DocumentCollection } from "../../utilities/sort-document-utils"; +import { DocumentGroup } from "../../models/stores/sorted-documents-documents-group"; import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; -import "./sort-work-view.scss"; +import "./sorted-section.scss"; interface IProps { docFilter: DocFilterType; + documentGroup: DocumentGroup; idx: number; - sortedSection: SortedDocument + secondarySort: SecondarySortType; } -export const SortedDocuments: React.FC = observer(function SortedDocuments(props: IProps) { - const { docFilter, idx, sortedSection } = props; +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); @@ -40,7 +42,7 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen }; const documentCount = () => { - const downloadedDocs = sortedSection.documents.filter(doc => getDocument(doc.key)); + const downloadedDocs = documentGroup.metaDataDocs?.filter((doc: IDocumentMetadata) => getDocument(doc.key)) ?? []; return downloadedDocs.length; }; @@ -54,8 +56,10 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen }; const renderDocumentItem = (doc: any) => { - const fullDocument = getDocument(doc.key); - if (docFilter === "Problem" && fullDocument) { + if (docFilter === "Problem" && secondarySort === "byNone") { + const fullDocument = docFilter === "Problem" ? getDocument(doc.key) : undefined; + if (!fullDocument) return
; + return = observer(function SortedDocumen allowDelete={false} onSelectDocument={handleSelectDocument} />; - } else if (docFilter === "Problem") { - return
; } else { return = observer(function SortedDocumen } }; + const renderList = () => { + if (secondarySort !== "byNone") { + return documentGroup[secondarySort]?.map((group: DocumentCollection) => { + return ( +
+
{group.label}
+ {group.documents?.map((doc: any) => { + const documentContext = getDocumentContext(doc); + return ( + + {renderDocumentItem(doc)} + + ); + })} +
+ ); + }); + } else { + return ( +
+ {documentGroup.metaDataDocs?.map((doc: any) => { + const documentContext = getDocumentContext(doc); + return ( + + {renderDocumentItem(doc)} + + ); + })} +
+ ); + } + }; + return ( -
+
- {sortedSection.icon ? : null} {sortedSection.sectionLabel} + {documentGroup.icon ? : null} {documentGroup.label}
Total workspaces: {documentCount()}
@@ -93,14 +128,7 @@ export const SortedDocuments: React.FC = observer(function SortedDocumen
- {showDocuments && sortedSection.documents.map((doc: any, sortIdx: number) => { - const documentContext = getDocumentContext(doc); - return ( - - {renderDocumentItem(doc)} - - ); - })} + {showDocuments && renderList()}
); diff --git a/src/components/thumbnail/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx index 2d5cc05cad..8494935ed3 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -6,8 +6,8 @@ import "./simple-document-item.scss"; interface IProps { document: IDocumentMetadata; - investigationOrdinal: number; - problemOrdinal: number; + investigationOrdinal: string; + problemOrdinal: string; onSelectDocument: (document: IDocumentMetadata) => void; } @@ -16,8 +16,8 @@ export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDoc const { uid } = document; const userName = classStore.getUserById(uid)?.displayName; const investigations = unit.investigations; - const investigation = investigations[investigationOrdinal]; - const problem = investigation?.problems[problemOrdinal - 1]; + const investigation = investigations[Number(investigationOrdinal)]; + const problem = investigation?.problems[Number(problemOrdinal) - 1]; const title = document.title ? `${userName}: ${document.title}` : `${userName}: ${problem?.title ?? "unknown title"}`; // TODO: Account for and use isPrivate in the view. isAccessibleToUser won't currently work here. // const isPrivate = !document.isAccessibleToUser(user, documents); diff --git a/src/models/stores/sorted-documents-documents-group.ts b/src/models/stores/sorted-documents-documents-group.ts index dfaef77e4f..ee390f0d41 100644 --- a/src/models/stores/sorted-documents-documents-group.ts +++ b/src/models/stores/sorted-documents-documents-group.ts @@ -1,3 +1,4 @@ +import { FC, SVGProps } from "react"; import { IDocumentMetadata } from "functions/src/shared"; import { ISortedDocumentsStores, TagWithDocs } from "./sorted-documents"; import { makeAutoObservable } from "mobx"; @@ -7,7 +8,7 @@ import { createDocMapByNames, createTileTypeToDocumentsMap, getTagsWithDocs, - SortedDocument, + DocumentCollection, sortGroupSectionLabels, sortNameSectionLabels } from "../../utilities/sort-document-utils"; @@ -16,82 +17,83 @@ import { getTileComponentInfo } from "../tiles/tile-component-info"; import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; +interface IDocumentGroup { + icon?:FC>; + label: string; + metaDataDocs: IDocumentMetadata[]; + stores: ISortedDocumentsStores; +} + export class DocumentGroup { stores: ISortedDocumentsStores; - value: string; + label: string; metaDataDocs: IDocumentMetadata[]; - sortCategory: string; // "Group", "Name", "Strategy", "Bookmark", "Tool" firestoreTagDocumentMap = new Map>(); + icon?: FC>; - constructor(stores: ISortedDocumentsStores, value: string, metaDataDocs: IDocumentMetadata[], sortCategory: string) { - makeAutoObservable(this); - this.stores = stores; - this.value = value; - this.metaDataDocs = metaDataDocs; - this.sortCategory = sortCategory; + constructor(props: IDocumentGroup) { + makeAutoObservable(this); + const { stores, label, metaDataDocs, icon } = props; + this.stores = stores; + this.label = label; + this.metaDataDocs = metaDataDocs; + this.icon = icon; } - get all(): SortedDocument[] { - return [{ - sectionLabel: this.value, - documents: this.metaDataDocs - }]; - } - - get groups(): SortedDocument[] { + get byGroup(): DocumentCollection[] { const documentMap = createDocMapByGroups(this.metaDataDocs, this.stores.groups.groupForUser); const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); - return sortedSectionLabels.map(sectionLabel => { + return sortedSectionLabels.map(label => { return { - sectionLabel, - documents: documentMap.get(sectionLabel)!.documents + label, + documents: documentMap.get(label)!.documents }; }); } - get names(): SortedDocument[] { + get byName(): DocumentCollection[] { const documentMap = createDocMapByNames(this.metaDataDocs, this.stores.class.getUserById); const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); - return sortedSectionLabels.map((sectionLabel) =>{ + return sortedSectionLabels.map((label) =>{ return { - sectionLabel, - documents: documentMap.get(sectionLabel).documents + label, + documents: documentMap.get(label).documents }; }); } - get strategies(): SortedDocument[] { + get byStrategy(): DocumentCollection[] { const commentTags = this.stores.appConfig.commentTags; const tagsWithDocs = getTagsWithDocs(this.metaDataDocs, commentTags, this.firestoreTagDocumentMap); - const sortedDocsArr: SortedDocument[] = []; + const sortedDocsArr: DocumentCollection[] = []; 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.metaDataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); sortedDocsArr.push({ - sectionLabel, + label, documents }); }); return sortedDocsArr; } - get tools(): SortedDocument[] { + get byTools(): DocumentCollection[] { const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.metaDataDocs); // Map the tile types to their display names - const sectionedDocuments = Object.keys(tileTypeToDocumentsMap).map(tileType => { - const section: SortedDocument = { - sectionLabel: tileType, - documents: tileTypeToDocumentsMap[tileType], + const sectionedDocuments = Array.from(tileTypeToDocumentsMap.keys()).map(tileType => { + const section: DocumentCollection = { + label: tileType, + documents: tileTypeToDocumentsMap.get(tileType)?.documents ?? [], }; if (tileType === "Sparrow") { section.icon = SparrowHeaderIcon; } else { const contentInfo = getTileContentInfo(tileType); - section.sectionLabel = contentInfo?.displayName || tileType; + section.label = contentInfo?.displayName || tileType; const componentInfo = getTileComponentInfo(tileType); section.icon = componentInfo?.HeaderIcon; } @@ -100,20 +102,20 @@ export class DocumentGroup { // 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; } - get bookmarks(): SortedDocument[] { + get byBookmarked(): DocumentCollection[] { const documentMap = createDocMapByBookmarks(this.metaDataDocs, this.stores.bookmarks); const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; return sortedSectionLabels.filter(label => documentMap.has(label)) .map(label => ({ - sectionLabel: label, + label, documents: documentMap.get(label).documents })); } diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 48c4b12f14..1abb1320e4 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -7,7 +7,6 @@ import { DB } from "../../lib/db"; import { AppConfigModelType } from "./app-config-model"; import { Bookmarks } from "./bookmarks"; import { UserModelType } from "./user"; -import { getTileContentInfo } from "../tiles/tile-content-info"; import { IDocumentMetadata } from "../../../functions/src/shared"; import { typeConverter } from "../../utilities/db-utils"; import { @@ -16,14 +15,15 @@ import { createDocMapByNames, createTileTypeToDocumentsMap, getTagsWithDocs, - SortedDocument, + DocumentCollection, sortGroupSectionLabels, sortNameSectionLabels } from "../../utilities/sort-document-utils"; import { DocumentGroup } from "./sorted-documents-documents-group"; +import { getTileContentInfo } from "../tiles/tile-content-info"; -export type SortedDocumentsMap = Record; +export type SortedDocumentsMap = Record; export type TagWithDocs = { tagKey: string; @@ -79,118 +79,69 @@ export class SortedDocuments { } // ** views ** // - get groups(): DocumentGroup[] { + get byGroup(): DocumentGroup[] { const documentMap = createDocMapByGroups(this.filteredDocsByType, this.groupsStore.groupForUser); const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); - return sortedSectionLabels.map(sectionLabel => { - return new DocumentGroup(this.stores, sectionLabel, documentMap.get(sectionLabel), "group"); + return sortedSectionLabels.map(label => { + return new DocumentGroup({stores: this.stores, label, metaDataDocs: documentMap.get(label).documents }); }); } - get names(): DocumentGroup[] { + get byName(): DocumentGroup[] { const documentMap = createDocMapByNames(this.filteredDocsByType, this.class.getUserById); const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); - return sortedSectionLabels.map((sectionLabel) =>{ - return new DocumentGroup(this.stores, sectionLabel, documentMap.get(sectionLabel).documents, "name"); + return sortedSectionLabels.map(label => { + return new DocumentGroup({ stores: this.stores, label, metaDataDocs: documentMap.get(label).documents }); }); } - get strategies(): DocumentGroup[] { + get byStrategy(): DocumentGroup[] { const commentTags = this.commentTags; const tagsWithDocs = getTagsWithDocs(this.firestoreMetadataDocs, commentTags, this.firestoreTagDocumentMap); 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.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); - sortedDocsArr.push(new DocumentGroup(this.stores, sectionLabel, documents, "strategy")); + sortedDocsArr.push(new DocumentGroup({ stores: this.stores, label, metaDataDocs: documents })); }); return sortedDocsArr; } - get tools(): DocumentGroup[] { + get byTools(): DocumentGroup[] { const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.firestoreMetadataDocs); - const sectionedDocuments = Object.keys(tileTypeToDocumentsMap).map(tileType => { + const sectionedDocuments = Array.from(tileTypeToDocumentsMap.keys()).map(tileType => { + const contentInfo = getTileContentInfo(tileType); - const value = contentInfo?.displayName || tileType; - const section: DocumentGroup = new DocumentGroup(this.stores, value, tileTypeToDocumentsMap[tileType], "tool"); + 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, metaDataDocs: documents, icon }); return section; }); // Sort the tile types. 'No Tools' should be at the end. const sortedByLabel = sectionedDocuments.sort((a, b) => { - if (a.value === "No Tools") return 1; // Move 'No Tools' to the end - if (b.value === "No Tools") return -1; // Alphabetically sort all others - return a.value.localeCompare(b.value); + 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 bookmarks(): DocumentGroup[] { + get byBookmarked(): DocumentGroup[] { const documentMap = createDocMapByBookmarks(this.firestoreMetadataDocs, this.bookmarksStore); const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; return sortedSectionLabels.filter(label => documentMap.has(label)) - .map(label => new DocumentGroup(this.stores, - label, documentMap.get(label).documents, "bookmark")); - } - - sortDocuments (primarySort: string, secondarySort: string) { - let documentGroups: DocumentGroup[] = []; - switch (primarySort) { - case "Group": - documentGroups = this.groups; - break; - case "Name": - documentGroups = this.names; - break; - case "Strategy": - documentGroups = this.strategies; - break; - case "Tool": - documentGroups = this.tools; - break; - case "Bookmark": - documentGroups = this.bookmarks; - break; - } - const sortedDocuments: SortedDocumentsMap = {}; - switch (secondarySort) { - case "None": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.all; - }); - break; - case "Group": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.groups; - }); - break; - case "Name": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.names; - }); - break; - case "Strategy": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.strategies; - }); - break; - case "Tool": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.tools; - }); - break; - case "Bookmark": - documentGroups.forEach(documentGroup => { - sortedDocuments[documentGroup.value] = documentGroup.bookmarks; - }); - break; - } - return sortedDocuments; + .map(label => new DocumentGroup({ + stores: this.stores, + label, + metaDataDocs: documentMap.get(label).documents + })); } async updateMetaDataDocs (filter: string, unit: string, investigation: number, problem: number) { diff --git a/src/models/stores/ui-types.ts b/src/models/stores/ui-types.ts index 9806ccc34a..d17802f2c0 100644 --- a/src/models/stores/ui-types.ts +++ b/src/models/stores/ui-types.ts @@ -4,6 +4,8 @@ export const UIDialogTypeEnum = types.enumeration("dialogType", ["alert", "confi export type UIDialogType = Instance; export const DocFilterTypeEnum = types.enumeration("docFilter", ["Problem", "Investigation", "Unit", "All"]); export type DocFilterType = Instance; +export type PrimarySortType = "byBookmarked" | "byGroup" | "byName" | "byStrategy" | "byTools"; +export type SecondarySortType = PrimarySortType | "byNone"; 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/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts index b8393f2364..a74f6c72be 100644 --- a/src/utilities/sort-document-utils.ts +++ b/src/utilities/sort-document-utils.ts @@ -1,8 +1,12 @@ import { IDocumentMetadata } from "functions/src/shared"; +import { FC, SVGProps } from "react"; import { Bookmarks } from "src/models/stores/bookmarks"; +import { getTileComponentInfo } from "../models/tiles/tile-component-info"; -export type SortedDocument = { - sectionLabel: string; +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 } @@ -128,13 +132,23 @@ export const getTagsWithDocs = (documents: IDocumentMetadata[], commentTags: Rec }; export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => { - const tileTypeToDocumentsMap: Record = {}; - + const tileTypeToDocumentsMap = new Map>(); const addDocByType = (docToAdd: IDocumentMetadata, type: string) => { - if (!tileTypeToDocumentsMap[type]) { - tileTypeToDocumentsMap[type] = []; + if (!tileTypeToDocumentsMap.get(type)) { + let icon: FC> | undefined; + if (type === "Sparrow") { + icon = SparrowHeaderIcon; + } else { + const componentInfo = getTileComponentInfo(type); + icon = componentInfo?.HeaderIcon; + } + tileTypeToDocumentsMap.set(type, { + icon, + documents: [] + } + ); } - tileTypeToDocumentsMap[type].push(docToAdd); + tileTypeToDocumentsMap.get(type)?.documents.push(docToAdd); }; //Iterate through all documents, determine if they are valid, From 5591032b9ffa54ace9304f0076fe057b3214f275 Mon Sep 17 00:00:00 2001 From: lublagg Date: Mon, 29 Jul 2024 17:51:33 -0400 Subject: [PATCH 016/127] Update and add tests for new modeling. --- src/components/document/sort-work-view.tsx | 2 +- src/components/document/sorted-section.tsx | 2 +- src/models/stores/document-group.test.ts | 328 ++++++++++++++++++ ...s-documents-group.ts => document-group.ts} | 0 src/models/stores/sorted-documents.test.ts | 108 +++--- src/models/stores/sorted-documents.ts | 2 +- 6 files changed, 385 insertions(+), 57 deletions(-) create mode 100644 src/models/stores/document-group.test.ts rename src/models/stores/{sorted-documents-documents-group.ts => document-group.ts} (100%) diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 6114da8a58..02c8fa29f9 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -9,7 +9,7 @@ import { ENavTab } from "../../models/view/nav-tabs"; import { DocListDebug } from "./doc-list-debug"; import { DocFilterType, PrimarySortType, SecondarySortType } from "../../models/stores/ui-types"; import { SortedSection } from "./sorted-section"; -import { DocumentGroup } from "../../models/stores/sorted-documents-documents-group"; +import { DocumentGroup } from "../../models/stores/document-group"; import "../thumbnail/document-type-collection.scss"; diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index 8d95739c85..fc1bdf79b7 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -12,7 +12,7 @@ import { SimpleDocumentItem } from "../thumbnail/simple-document-item"; import { IDocumentMetadata } from "../../../functions/src/shared"; import { DocumentContextReact } from "./document-context"; import { DocumentCollection } from "../../utilities/sort-document-utils"; -import { DocumentGroup } from "../../models/stores/sorted-documents-documents-group"; +import { DocumentGroup } from "../../models/stores/document-group"; import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; diff --git a/src/models/stores/document-group.test.ts b/src/models/stores/document-group.test.ts new file mode 100644 index 0000000000..29f40d09d9 --- /dev/null +++ b/src/models/stores/document-group.test.ts @@ -0,0 +1,328 @@ +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 "../../../functions/src/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, + tileTypes: [], + strategies: ["foo", "bar"], + }, + { + uid: "2", //Scott + type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, + tileTypes: ["Text"] + }, + { + uid: "3", //Dennis + type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, + tileTypes: ["Drawing"] + }, + { + uid: "4", //Kirk + type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, + tileTypes: [], + 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.only('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(1); + expect(documentCollection2[0].label).toBe("Bookmarked"); + expect(documentCollection2[0].documents.length).toBe(1); + + const documentCollection3 = documentsByGroup[2].byBookmarked; + expect(documentCollection3.length).toBe(1); + expect(documentCollection3[0].label).toBe("Not Bookmarked"); + expect(documentCollection3[0].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; + console.log("documentCollection!!", documentCollection); + 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/sorted-documents-documents-group.ts b/src/models/stores/document-group.ts similarity index 100% rename from src/models/stores/sorted-documents-documents-group.ts rename to src/models/stores/document-group.ts diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 6162e09012..c3a7e5efa1 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -163,58 +163,58 @@ describe('Sorted Documents Model', () => { }); - // describe('sortByGroup Function', () => { - // it('should correctly sort documents by group', () => { - // const sortedDocsByGroup = sortedDocuments.sortByGroup; - // expect(sortedDocsByGroup.length).toBe(3); - // const group3 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 3'); - // expect(group3?.documents.length).toBe(2); // Group 3 - Kirk + Scott - // const group5 = sortedDocsByGroup.find(group => group.sectionLabel === 'Group 5'); - // expect(group5?.documents.length).toBe(1); // Group 5 - Joe - // const group9 = sortedDocsByGroup.find(group => group.sectionLabel === '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); - // expect(sortedSectionLabels).toEqual(['Group 3', 'Group 5', 'Group 9']); - // }); - // }); - - // describe('sortByName Function', () => { - // it('should correctly sort documents by last name', () => { - // const expectedOrder = [ - // "Bacal, Joe", - // "Cao, Dennis", - // "Cytacki, Scott", - // "Swenson, Kirk" - // ]; - // const sortedDocsByName = sortedDocuments.sortByName; - // const actualOrder = sortedDocsByName.map(group => group.sectionLabel); - // expect(actualOrder).toEqual(expectedOrder); - // }); - // }); - - // describe('sortByTools Function', () => { - // it('should correctly sort documents by tool', () => { - // const sortedDocsByTools = sortedDocuments.sortByTools; - // const summaryOfResult = sortedDocsByTools.map(section => ({ - // sectionLabel: section.sectionLabel, - // docKeys: section.documents.map(doc => doc.key) - // })); - // expect(summaryOfResult).toEqual([ - // { sectionLabel: "Sketch", docKeys: [ - // "Student 3 Problem Doc Group 9" - // ]}, - // { sectionLabel: "Text", docKeys: [ - // "Student 2 Problem Doc Group 3" - // ]}, - // { sectionLabel: "No Tools", docKeys: [ - // "Student 1 Problem Doc Group 5", - // "Student 4 Problem Doc Group 3" - // ]} - // ]); - // }); - // }); + describe('byGroup Function', () => { + it('should correctly sort documents by group', () => { + const sortedDocsByGroup = sortedDocuments.byGroup; + expect(sortedDocsByGroup.length).toBe(3); + const group3 = sortedDocsByGroup.find(group => group.label === 'Group 3'); + expect(group3?.metaDataDocs.length).toBe(2); // Group 3 - Kirk + Scott + const group5 = sortedDocsByGroup.find(group => group.label === 'Group 5'); + expect(group5?.metaDataDocs.length).toBe(1); // Group 5 - Joe + const group9 = sortedDocsByGroup.find(group => group.label === 'Group 9'); + expect(group9?.metaDataDocs.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.byGroup.map(group => group.label); + expect(sortedSectionLabels).toEqual(['Group 3', 'Group 5', 'Group 9']); + }); + }); + + describe('byName Function', () => { + it('should correctly sort documents by last name', () => { + const expectedOrder = [ + "Bacal, Joe", + "Cao, Dennis", + "Cytacki, Scott", + "Swenson, Kirk" + ]; + const sortedDocsByName = sortedDocuments.byName; + const actualOrder = sortedDocsByName.map(group => group.label); + expect(actualOrder).toEqual(expectedOrder); + }); + }); + + describe('byTools Function', () => { + it('should correctly sort documents by tool', () => { + const sortedDocsByTools = sortedDocuments.byTools; + const summaryOfResult = sortedDocsByTools.map(section => ({ + sectionLabel: section.label, + docKeys: section.metaDataDocs.map(doc => doc.key) + })); + expect(summaryOfResult).toEqual([ + { sectionLabel: "Sketch", docKeys: [ + "Student 3 Problem Doc Group 9" + ]}, + { sectionLabel: "Text", docKeys: [ + "Student 2 Problem Doc Group 3" + ]}, + { sectionLabel: "No Tools", docKeys: [ + "Student 1 Problem Doc Group 5", + "Student 4 Problem Doc Group 3" + ]} + ]); + }); + }); }); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 1abb1320e4..4c54811f48 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -19,7 +19,7 @@ import { sortGroupSectionLabels, sortNameSectionLabels } from "../../utilities/sort-document-utils"; -import { DocumentGroup } from "./sorted-documents-documents-group"; +import { DocumentGroup } from "./document-group"; import { getTileContentInfo } from "../tiles/tile-content-info"; From 6eeca4eeade666b8d2554251e0e81fbf91a26270 Mon Sep 17 00:00:00 2001 From: lublagg Date: Mon, 29 Jul 2024 17:52:48 -0400 Subject: [PATCH 017/127] Remove 'only' from document-group test. --- src/models/stores/document-group.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/stores/document-group.test.ts b/src/models/stores/document-group.test.ts index 29f40d09d9..cb31f21f75 100644 --- a/src/models/stores/document-group.test.ts +++ b/src/models/stores/document-group.test.ts @@ -182,7 +182,7 @@ describe('DocumentGroup Model', () => { }); describe("byBookMarked Function", () => { - it.only('should return a doc collection sorted by bookmarks and with the correct documents per bookmark', () => { + 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), From 4cee3322c2017ba5dcac0a80e357b49092401624 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 29 Jul 2024 18:29:22 -0400 Subject: [PATCH 018/127] chore: update existing cypress tests --- .../teacher_sort_work_view_spec.js | 30 +++++++++---------- cypress/support/elements/common/SortedWork.js | 19 +++++++----- 2 files changed, 26 insertions(+), 23 deletions(-) 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 7c44492d4c..64c463dcd8 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 @@ -45,23 +45,23 @@ 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'); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.getSortWorkItem().eq(1).click(); // Open the first document in the list resourcesPanel.getEditableDocumentContent().should('be.visible'); - resourcesPanel.getDocumentCloseButton().click(); + resourcesPanel.getDocumentCloseButton().click({ force: true }); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed }); @@ -209,9 +209,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" ]); @@ -220,9 +220,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]); @@ -235,10 +235,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]); @@ -252,8 +252,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/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index b4ca1390d9..d7a7bb4f1b 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -1,15 +1,15 @@ 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"]'); } getSortWorkItem() { return cy.get(".sort-work-view .sorted-sections .list-item .footer .info"); @@ -20,6 +20,9 @@ class SortedWork { getSortWorkGroup(groupName) { return cy.get(".sort-work-view .sorted-sections .section-header-label").contains(groupName).parent().parent().parent(); } + getSecondarySortByMenu() { + return cy.get('.custom-select.sort-work-sort-menu.secondary-sort-menu'); + } getShowForMenu() { return cy.get("[data-test=filter-work-menu]"); } From 16b24a2c4a93fe3a109b5766b101be6d489f75e0 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 29 Jul 2024 18:29:57 -0400 Subject: [PATCH 019/127] chore: add "then" text between sort menus --- src/components/navigation/sort-work-header.scss | 7 ++++++- src/components/navigation/sort-work-header.tsx | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/navigation/sort-work-header.scss b/src/components/navigation/sort-work-header.scss index 8ad06a8303..4fd468b837 100644 --- a/src/components/navigation/sort-work-header.scss +++ b/src/components/navigation/sort-work-header.scss @@ -15,6 +15,11 @@ align-items: center; display: flex; margin: 0 6px 0 8px; + + + &.secondary { + margin: 0 5px 0 0; + } } .header-dropdown { @@ -24,7 +29,7 @@ .custom-select.sort-work-sort-menu, .custom-select.filter-work-menu { color: $charcoal-dark-2; - margin-right: 8px; + margin-right: 5px; .header { min-width: 151px; diff --git a/src/components/navigation/sort-work-header.tsx b/src/components/navigation/sort-work-header.tsx index 7b8859b168..17a599f6ba 100644 --- a/src/components/navigation/sort-work-header.tsx +++ b/src/components/navigation/sort-work-header.tsx @@ -28,6 +28,7 @@ export const SortWorkHeader:React.FC= observer(function SortWo showItemChecks={true} />
+
then
Date: Mon, 29 Jul 2024 20:11:53 -0400 Subject: [PATCH 020/127] chore: undo new stylesheet for SortedSection --- .../teacher_sort_work_view_spec.js | 2 +- src/components/document/sort-work-view.scss | 71 ++++++++++++++++++ src/components/document/sorted-section.scss | 73 ------------------- src/components/document/sorted-section.tsx | 2 +- 4 files changed, 73 insertions(+), 75 deletions(-) delete mode 100644 src/components/document/sorted-section.scss 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 64c463dcd8..53d3c6af56 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 @@ -61,7 +61,7 @@ describe('SortWorkView Tests', () => { cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.getSortWorkItem().eq(1).click(); // Open the first document in the list resourcesPanel.getEditableDocumentContent().should('be.visible'); - resourcesPanel.getDocumentCloseButton().click({ force: true }); + resourcesPanel.getDocumentCloseButton().click(); cy.get('.section-header-arrow').click({multiple: true}); // Open the sections sortWork.getSortWorkItem().should('be.visible'); // Verify the document is closed }); diff --git a/src/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index eeca42f058..3f8e6ca82b 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -12,6 +12,77 @@ $title-margin: 2px; border-top: none; } + .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); + } + } + } + } + } + .loading-spinner { + background-image: url("../../assets/Spinner-1s-200px.svg"); + background-size: contain; + background-repeat: no-repeat; + width: 100px; + height: 75px; + } + .list { + .doc-group { + display: flex; + width: 100%; + } + } + } + .focus-document { display: flex; flex-direction: column; diff --git a/src/components/document/sorted-section.scss b/src/components/document/sorted-section.scss deleted file mode 100644 index fbaf4588ad..0000000000 --- a/src/components/document/sorted-section.scss +++ /dev/null @@ -1,73 +0,0 @@ -@import "../vars"; - -.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); - } - } - } - } - } - .loading-spinner { - background-image: url("../../assets/Spinner-1s-200px.svg"); - background-size: contain; - background-repeat: no-repeat; - width: 100px; - height: 75px; - } - .list { - - .doc-group { - display: flex; - width: 100%; - } - } -} diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index fc1bdf79b7..4f00959b9f 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -16,7 +16,7 @@ import { DocumentGroup } from "../../models/stores/document-group"; import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; -import "./sorted-section.scss"; +import "./sort-work-view.scss"; interface IProps { docFilter: DocFilterType; From 834bb6d92d5321e8e3b4ed732f54244f819415d4 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 30 Jul 2024 16:57:37 -0400 Subject: [PATCH 021/127] chore: code review suggestions --- src/components/document/sort-work-view.scss | 1 + src/components/document/sort-work-view.tsx | 15 +++--- src/components/document/sorted-section.tsx | 8 ++-- .../thumbnail/simple-document-item.scss | 2 + src/models/stores/document-group.test.ts | 13 +++-- src/models/stores/document-group.ts | 47 ++++++++++++------- src/models/stores/sorted-documents.ts | 24 ++++++++-- src/models/stores/ui-types.ts | 4 +- src/utilities/sort-document-utils.ts | 27 ++++------- 9 files changed, 84 insertions(+), 57 deletions(-) diff --git a/src/components/document/sort-work-view.scss b/src/components/document/sort-work-view.scss index 3f8e6ca82b..85cf4636fa 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -78,6 +78,7 @@ $title-margin: 2px; .list { .doc-group { display: flex; + flex-wrap: wrap; width: 100%; } } diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index 02c8fa29f9..b12e4718b1 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -40,14 +40,13 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { onClick: () => setPrimarySortBy(option) })); - const secondarySortOptions: ICustomDropdownItem[] = []; - secondarySortOptions.push({ text: "None", onClick: () => setSecondarySortBy("None") }); - sortOptions.map((option) => secondarySortOptions.push({ + const secondarySortOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ disabled: option === primarySortBy, selected: option === secondarySortBy, text: option, onClick: () => setSecondarySortBy(option) })); + secondarySortOptions.unshift({ text: "None", onClick: () => setSecondarySortBy("None") }); const docFilterOptions: ICustomDropdownItem[] = filterOptions.map((option) => ({ selected: option === docFilter, @@ -55,12 +54,10 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { onClick: () => handleDocFilterSelection(option) })); - const primarySearchTerm = primarySortBy === sortTagPrompt ? "byStrategy" : `by${primarySortBy}` as PrimarySortType; - const secondarySearchTerm = secondarySortBy === sortTagPrompt - ? "byStrategy" - : `by${secondarySortBy}` as SecondarySortType; - const sortedDocumentGroups = sortedDocuments[primarySearchTerm]; - + 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; diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index 4f00959b9f..8f89aaa59f 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -56,8 +56,8 @@ export const SortedSection: React.FC = observer(function SortedDocuments }; const renderDocumentItem = (doc: any) => { - if (docFilter === "Problem" && secondarySort === "byNone") { - const fullDocument = docFilter === "Problem" ? getDocument(doc.key) : undefined; + const fullDocument = docFilter === "Problem" ? getDocument(doc.key) : undefined; + if (docFilter === "Problem" && secondarySort === "None") { if (!fullDocument) return
; return = observer(function SortedDocuments }; const renderList = () => { - if (secondarySort !== "byNone") { - return documentGroup[secondarySort]?.map((group: DocumentCollection) => { + if (secondarySort !== "None") { + return documentGroup.sortBy(secondarySort)?.map((group: DocumentCollection) => { return (
{group.label}
diff --git a/src/components/thumbnail/simple-document-item.scss b/src/components/thumbnail/simple-document-item.scss index 5371b4242c..256ddb6525 100644 --- a/src/components/thumbnail/simple-document-item.scss +++ b/src/components/thumbnail/simple-document-item.scss @@ -5,6 +5,8 @@ border: solid 1px #707070; border-radius: 1px; cursor: pointer; + display: flex; + flex-shrink: 0; height: 12px; margin: 2px 5px; width: 12px; diff --git a/src/models/stores/document-group.test.ts b/src/models/stores/document-group.test.ts index cb31f21f75..8c1ee1bf69 100644 --- a/src/models/stores/document-group.test.ts +++ b/src/models/stores/document-group.test.ts @@ -202,14 +202,18 @@ describe('DocumentGroup Model', () => { expect(documentCollection[1].documents.length).toBe(1); const documentCollection2 = documentsByGroup[1].byBookmarked; - expect(documentCollection2.length).toBe(1); + 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(1); - expect(documentCollection3[0].label).toBe("Not Bookmarked"); - expect(documentCollection3[0].documents.length).toBe(1); + 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); }); }); @@ -260,7 +264,6 @@ describe('DocumentGroup Model', () => { it('should return a document collection sorted by strategy with the correct documents per strategy', () => { const documentGroup = sortedDocuments.byName[0]; const documentCollection = documentGroup.byStrategy; - console.log("documentCollection!!", documentCollection); 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); diff --git a/src/models/stores/document-group.ts b/src/models/stores/document-group.ts index ee390f0d41..a3fa05f8f4 100644 --- a/src/models/stores/document-group.ts +++ b/src/models/stores/document-group.ts @@ -14,6 +14,7 @@ import { } 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"; @@ -40,26 +41,44 @@ export class DocumentGroup { this.icon = icon; } - get byGroup(): DocumentCollection[] { - const documentMap = createDocMapByGroups(this.metaDataDocs, this.stores.groups.groupForUser); - const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); + buildDocumentCollection( + sortedSectionLabels: string[], docMap: Map + ): DocumentCollection[] { return sortedSectionLabels.map(label => { return { label, - documents: documentMap.get(label)!.documents + documents: docMap.get(label) ?? [] }; }); } + sortBy(sortType: SecondarySortType): DocumentCollection[] { + 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(): DocumentCollection[] { + const documentMap = createDocMapByGroups(this.metaDataDocs, this.stores.groups.groupForUser); + const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); + return this.buildDocumentCollection(sortedSectionLabels, documentMap); + } + get byName(): DocumentCollection[] { const documentMap = createDocMapByNames(this.metaDataDocs, this.stores.class.getUserById); const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); - return sortedSectionLabels.map((label) =>{ - return { - label, - documents: documentMap.get(label).documents - }; - }); + return this.buildDocumentCollection(sortedSectionLabels, documentMap); } get byStrategy(): DocumentCollection[] { @@ -113,10 +132,6 @@ export class DocumentGroup { get byBookmarked(): DocumentCollection[] { const documentMap = createDocMapByBookmarks(this.metaDataDocs, this.stores.bookmarks); const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; - return sortedSectionLabels.filter(label => documentMap.has(label)) - .map(label => ({ - label, - documents: documentMap.get(label).documents - })); - } + return this.buildDocumentCollection(sortedSectionLabels, documentMap); + } } diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 4c54811f48..39cc447375 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -21,6 +21,7 @@ import { } from "../../utilities/sort-document-utils"; import { DocumentGroup } from "./document-group"; import { getTileContentInfo } from "../tiles/tile-content-info"; +import { PrimarySortType } from "./ui-types"; export type SortedDocumentsMap = Record; @@ -78,19 +79,36 @@ export class SortedDocuments { return this.stores.user; } + 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 []; + } + } + // ** 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, metaDataDocs: documentMap.get(label).documents }); + return new DocumentGroup({stores: this.stores, label, metaDataDocs: documentMap.get(label) ?? [] }); }); } 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, metaDataDocs: documentMap.get(label).documents }); + return new DocumentGroup({ stores: this.stores, label, metaDataDocs: documentMap.get(label) ?? [] }); }); } @@ -140,7 +158,7 @@ export class SortedDocuments { .map(label => new DocumentGroup({ stores: this.stores, label, - metaDataDocs: documentMap.get(label).documents + metaDataDocs: documentMap.get(label) ?? [] })); } diff --git a/src/models/stores/ui-types.ts b/src/models/stores/ui-types.ts index d17802f2c0..cb2f9e2fb5 100644 --- a/src/models/stores/ui-types.ts +++ b/src/models/stores/ui-types.ts @@ -4,8 +4,8 @@ export const UIDialogTypeEnum = types.enumeration("dialogType", ["alert", "confi export type UIDialogType = Instance; export const DocFilterTypeEnum = types.enumeration("docFilter", ["Problem", "Investigation", "Unit", "All"]); export type DocFilterType = Instance; -export type PrimarySortType = "byBookmarked" | "byGroup" | "byName" | "byStrategy" | "byTools"; -export type SecondarySortType = PrimarySortType | "byNone"; +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/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts index a74f6c72be..ee57c9bdbb 100644 --- a/src/utilities/sort-document-utils.ts +++ b/src/utilities/sort-document-utils.ts @@ -18,19 +18,16 @@ type TagWithDocs = { }; export const createDocMapByGroups = (documents: IDocumentMetadata[], groupForUser: (userId: string) => any) => { - const documentMap = new Map(); + 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, { - sectionLabel, - documents: [] - }); + documentMap.set(sectionLabel, []); } - documentMap.get(sectionLabel).documents.push(doc); + documentMap.get(sectionLabel)?.push(doc); }); return documentMap; }; @@ -44,17 +41,14 @@ export const sortGroupSectionLabels = (docMapKeys: string[]) => { }; export const createDocMapByNames = (documents: IDocumentMetadata[], getUserById: (uid: string) => any) => { - const documentMap = new Map(); + 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, { - sectionLabel, - documents: [] - }); + documentMap.set(sectionLabel, []); } - documentMap.get(sectionLabel).documents.push(doc); + documentMap.get(sectionLabel)?.push(doc); }); return documentMap; }; @@ -171,16 +165,13 @@ export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => }; export const createDocMapByBookmarks = (documents: IDocumentMetadata[], bookmarks: Bookmarks) => { - const documentMap = new Map(); + const documentMap: Map = new Map(); documents.forEach((doc) => { const sectionLabel = bookmarks.isDocumentBookmarked(doc.key) ? "Bookmarked" : "Not Bookmarked"; if (!documentMap.has(sectionLabel)) { - documentMap.set(sectionLabel, { - sectionLabel, - documents: [] - }); + documentMap.set(sectionLabel, []); } - documentMap.get(sectionLabel).documents.push(doc); + documentMap.get(sectionLabel)?.push(doc); }); return documentMap; }; From 54d0e314f06f1970f686a167f7e437634a2c1353 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 31 Jul 2024 11:39:57 -0400 Subject: [PATCH 022/127] chore: more code review improvements --- src/components/document/sorted-section.tsx | 8 ++------ .../thumbnail/simple-document-item.tsx | 3 +++ src/models/stores/document-group.ts | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index 8f89aaa59f..8f35de346c 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -29,6 +29,7 @@ export const SortedSection: React.FC = observer(function SortedDocuments const { docFilter, documentGroup, idx, secondarySort } = props; const { persistentUI, sortedDocuments } = useStores(); const [showDocuments, setShowDocuments] = useState(false); + const documentCount = documentGroup.metaDataDocs?.length || 0; const getDocument = (docKey: string) => { const document = sortedDocuments.documents.all.find((doc: DocumentModelType) => doc.key === docKey); @@ -41,11 +42,6 @@ export const SortedSection: React.FC = observer(function SortedDocuments return undefined; }; - const documentCount = () => { - const downloadedDocs = documentGroup.metaDataDocs?.filter((doc: IDocumentMetadata) => getDocument(doc.key)) ?? []; - return downloadedDocs.length; - }; - const handleSelectDocument = async (document: DocumentModelType | IDocumentMetadata) => { persistentUI.openSubTabDocument(ENavTab.kSortWork, ENavTab.kSortWork, document.key); logDocumentViewEvent(document); @@ -119,7 +115,7 @@ export const SortedSection: React.FC = observer(function SortedDocuments {documentGroup.icon ? : null} {documentGroup.label}
-
Total workspaces: {documentCount()}
+
Total workspaces: {documentCount}
Date: Wed, 31 Jul 2024 16:28:41 -0400 Subject: [PATCH 023/127] Checkpoint: Update document metadata when document tiles change. --- .../document/editable-document-content.tsx | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/document/editable-document-content.tsx b/src/components/document/editable-document-content.tsx index 26b87e638a..9f98a702a8 100644 --- a/src/components/document/editable-document-content.tsx +++ b/src/components/document/editable-document-content.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react"; import classNames from "classnames"; -import { clone } from "mobx-state-tree"; +import { clone, onSnapshot } from "mobx-state-tree"; import { AppConfigContext } from "../../app-config-context"; import { CanvasComponent } from "./canvas"; import { DocumentContextReact } from "./document-context"; @@ -10,10 +10,11 @@ import { useDocumentSyncToFirebase } from "../../hooks/use-document-sync-to-fire import { useGroupsStore, useStores } from "../../hooks/use-stores"; import { ToolbarComponent } from "../toolbar"; import { EditableTileApiInterfaceRef, EditableTileApiInterfaceRefContext } from "../tiles/tile-api"; -import { DocumentModelType } from "../../models/document/document"; +import { DocumentModelSnapshotType, DocumentModelType } from "../../models/document/document"; import { ProblemDocument } from "../../models/document/document-types"; import { IToolbarModel } from "../../models/stores/problem-configuration"; import { WorkspaceMode } from "../../models/stores/workspace"; +import { ITileMapEntry } from "../../../functions/src/shared"; import "./editable-document-content.scss"; @@ -101,7 +102,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,6 +115,33 @@ export function EditableDocumentContent({ contained ? "contained-editable-document-content" : "full-screen-editable-document-content", {"comment-select" : documentSelectedForComment, "full-height": fullHeight}, className); + useEffect(() => { + const disposer = onSnapshot(document, (snapshot: DocumentModelSnapshotType) => { + const tileMap = snapshot.content?.tileMap || {}; + const tileTypes: string[] = []; + + Object.keys(tileMap).forEach((key) => { + const tileInfo = tileMap[key] as ITileMapEntry; + const type = tileInfo.content.type; + if (!tileTypes.includes(type)) { + tileTypes.push(type); + } + }); + + // update tiletypes for metadata document in firestore + const query = firestore.collection("documents").where("key", "==", document.key); + query.get().then((querySnapshot) => { + querySnapshot.forEach((doc) => { + const docRef = doc.ref; + docRef.update({ + tileTypes, + }); + }); + }); + }); + return () => disposer(); + }, [document, firestore]); + useDocumentSyncToFirebase(user, firebase, document, readOnly); return ( From fa850082d1f2de72e4624dd26b7bc2181df1b63d Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 31 Jul 2024 11:39:57 -0400 Subject: [PATCH 024/127] feat: Filters Arrange docs boxes in rows (PT-187924176) [#187924176](https://www.pivotaltracker.com/story/show/187924176) --- .../teacher_sort_work_view_spec.js | 77 +++++++++- cypress/support/elements/common/SortedWork.js | 18 +++ src/assets/workspace-instance-scroll.svg | 6 + src/components/document/document-group.scss | 112 ++++++++++++++ src/components/document/document-group.tsx | 141 ++++++++++++++++++ src/components/document/sort-work-view.scss | 72 --------- src/components/document/sort-work-view.tsx | 23 ++- src/components/document/sorted-section.scss | 103 +++++++++++++ src/components/document/sorted-section.tsx | 108 ++++++-------- .../thumbnail/document-type-collection.scss | 3 +- .../thumbnail/simple-document-item.scss | 14 +- src/models/stores/document-group.ts | 68 ++++----- src/models/stores/sorted-documents.test.ts | 8 +- src/models/stores/sorted-documents.ts | 14 +- 14 files changed, 575 insertions(+), 192 deletions(-) create mode 100644 src/assets/workspace-instance-scroll.svg create mode 100644 src/components/document/document-group.scss create mode 100644 src/components/document/document-group.tsx create mode 100644 src/components/document/sorted-section.scss 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 53d3c6af56..f48773d4c2 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 @@ -103,7 +103,82 @@ describe('SortWorkView Tests', () => { sortWork.getFocusDocument().should("be.visible"); }); - // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. + 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.wait(500); + 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.wait(500); + 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"); + + }); + + // 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", () => { // Clear data before the test so it can be retried and will start with a clean slate cy.clearQAData('all'); diff --git a/cypress/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index d7a7bb4f1b..b7bdb034bd 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -23,6 +23,24 @@ class SortedWork { 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]"); } 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/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..e4ffcc2e77 --- /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 "../../../functions/src/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/sort-work-view.scss b/src/components/document/sort-work-view.scss index 85cf4636fa..eeca42f058 100644 --- a/src/components/document/sort-work-view.scss +++ b/src/components/document/sort-work-view.scss @@ -12,78 +12,6 @@ $title-margin: 2px; border-top: none; } - .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); - } - } - } - } - } - .loading-spinner { - background-image: url("../../assets/Spinner-1s-200px.svg"); - background-size: contain; - background-repeat: no-repeat; - width: 100px; - height: 75px; - } - .list { - .doc-group { - display: flex; - flex-wrap: wrap; - width: 100%; - } - } - } - .focus-document { display: flex; flex-direction: column; diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index b12e4718b1..b077f9ab32 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -33,11 +33,18 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { persistentUI.setDocFilter(filter); }; + const handlePrimarySortBySelection = (sort: string) => { + setPrimarySortBy(sort); + if (sort === secondarySortBy) { + setSecondarySortBy("None"); + } + }; + const primarySortByOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ disabled: false, selected: option === primarySortBy, text: option, - onClick: () => setPrimarySortBy(option) + onClick: () => handlePrimarySortBySelection(option) })); const secondarySortOptions: ICustomDropdownItem[] = sortOptions.map((option) => ({ @@ -46,7 +53,12 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { text: option, onClick: () => setSecondarySortBy(option) })); - secondarySortOptions.unshift({ text: "None", onClick: () => setSecondarySortBy("None") }); + secondarySortOptions.unshift({ + disabled: false, + selected: secondarySortBy === "None", + text: "None", + onClick: () => setSecondarySortBy("None") + }); const docFilterOptions: ICustomDropdownItem[] = filterOptions.map((option) => ({ selected: option === docFilter, @@ -66,12 +78,6 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { sortedDocuments.updateMetaDataDocs(docFilter, unit.code, investigation.ordinal, problem.ordinal); }, [docFilter, unit.code, investigation.ordinal, problem.ordinal, sortedDocuments]); - useEffect(() => { - if (primarySortBy === secondarySortBy) { - setSecondarySortBy("None"); - } - }, [primarySortBy, secondarySortBy]); - return (
{ @@ -79,6 +85,7 @@ export const SortWorkView: React.FC = observer(function SortWorkView() { : <> = observer(function SortedDocuments const { docFilter, documentGroup, idx, secondarySort } = props; const { persistentUI, sortedDocuments } = useStores(); const [showDocuments, setShowDocuments] = useState(false); - const documentCount = documentGroup.metaDataDocs?.length || 0; + const documentCount = documentGroup.documents?.length || 0; const getDocument = (docKey: string) => { const document = sortedDocuments.documents.all.find((doc: DocumentModelType) => doc.key === docKey); @@ -51,68 +52,48 @@ export const SortedSection: React.FC = observer(function SortedDocuments setShowDocuments(!showDocuments); }; - const renderDocumentItem = (doc: any) => { - const fullDocument = docFilter === "Problem" ? getDocument(doc.key) : undefined; - if (docFilter === "Problem" && secondarySort === "None") { - if (!fullDocument) return
; - - return ; - } else { - return ; - } + const renderUngroupedDocument = (doc: IDocumentMetadata) => { + const fullDocument = getDocument(doc.key); + if (!fullDocument) return
; + + return ; }; const renderList = () => { - if (secondarySort !== "None") { - return documentGroup.sortBy(secondarySort)?.map((group: DocumentCollection) => { - return ( -
-
{group.label}
- {group.documents?.map((doc: any) => { - const documentContext = getDocumentContext(doc); - return ( - - {renderDocumentItem(doc)} - - ); - })} -
- ); - }); - } else { - return ( -
- {documentGroup.metaDataDocs?.map((doc: any) => { - const documentContext = getDocumentContext(doc); - return ( - - {renderDocumentItem(doc)} - - ); - })} -
- ); + 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 ? : null} {documentGroup.label} + {documentGroup.icon && } {documentGroup.label}
Total workspaces: {documentCount}
@@ -123,7 +104,12 @@ export const SortedSection: React.FC = observer(function SortedDocuments
-
+ {secondarySort !== "None" && +
+ {secondarySort} +
+ } +
{showDocuments && renderList()}
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/simple-document-item.scss b/src/components/thumbnail/simple-document-item.scss index 256ddb6525..0274703d37 100644 --- a/src/components/thumbnail/simple-document-item.scss +++ b/src/components/thumbnail/simple-document-item.scss @@ -6,8 +6,16 @@ border-radius: 1px; cursor: pointer; display: flex; + flex-grow: 0; flex-shrink: 0; - height: 12px; - margin: 2px 5px; - width: 12px; + height: 16px; + margin: 0; + width: 16px; + + &:hover { + background: $classwork-purple-light-4; + } + &:active { + background: $classwork-purple-light-2; + } } diff --git a/src/models/stores/document-group.ts b/src/models/stores/document-group.ts index 4c54a6fc43..5b680f1d20 100644 --- a/src/models/stores/document-group.ts +++ b/src/models/stores/document-group.ts @@ -8,7 +8,6 @@ import { createDocMapByNames, createTileTypeToDocumentsMap, getTagsWithDocs, - DocumentCollection, sortGroupSectionLabels, sortNameSectionLabels } from "../../utilities/sort-document-utils"; @@ -21,7 +20,7 @@ import SparrowHeaderIcon from "../../assets/icons/sort-by-tools/sparrow-id.svg"; interface IDocumentGroup { icon?:FC>; label: string; - metaDataDocs: IDocumentMetadata[]; + documents: IDocumentMetadata[]; stores: ISortedDocumentsStores; } @@ -45,31 +44,30 @@ interface IDocumentGroup { export class DocumentGroup { stores: ISortedDocumentsStores; label: string; - metaDataDocs: IDocumentMetadata[]; + documents: IDocumentMetadata[]; firestoreTagDocumentMap = new Map>(); icon?: FC>; constructor(props: IDocumentGroup) { makeAutoObservable(this); - const { stores, label, metaDataDocs, icon } = props; + const { stores, label, documents, icon } = props; this.stores = stores; this.label = label; - this.metaDataDocs = metaDataDocs; + this.documents = documents; this.icon = icon; } - buildDocumentCollection( - sortedSectionLabels: string[], docMap: Map - ): DocumentCollection[] { + buildDocumentCollection(sortedSectionLabels: string[], docMap: Map): DocumentGroup[] { return sortedSectionLabels.map(label => { - return { + return new DocumentGroup({ label, - documents: docMap.get(label) ?? [] - }; + documents: docMap.get(label) ?? [], + stores: this.stores + }); }); } - sortBy(sortType: SecondarySortType): DocumentCollection[] { + sortBy(sortType: SecondarySortType): DocumentGroup[] { switch (sortType) { case "Group": return this.byGroup; @@ -86,45 +84,47 @@ export class DocumentGroup { } } - get byGroup(): DocumentCollection[] { - const documentMap = createDocMapByGroups(this.metaDataDocs, this.stores.groups.groupForUser); - const sortedSectionLabels = sortGroupSectionLabels(Array.from(documentMap.keys())); - return this.buildDocumentCollection(sortedSectionLabels, documentMap); + 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(): DocumentCollection[] { - const documentMap = createDocMapByNames(this.metaDataDocs, this.stores.class.getUserById); - const sortedSectionLabels = sortNameSectionLabels(Array.from(documentMap.keys())); - return this.buildDocumentCollection(sortedSectionLabels, documentMap); + 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(): DocumentCollection[] { + get byStrategy(): DocumentGroup[] { const commentTags = this.stores.appConfig.commentTags; - const tagsWithDocs = getTagsWithDocs(this.metaDataDocs, commentTags, this.firestoreTagDocumentMap); + const tagsWithDocs = getTagsWithDocs(this.documents, commentTags, this.firestoreTagDocumentMap); - const sortedDocsArr: DocumentCollection[] = []; + 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.metaDataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); - sortedDocsArr.push({ + const documents = this.documents.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); + sortedDocsArr.push(new DocumentGroup({ label, - documents - }); + documents, + stores: this.stores + })); }); return sortedDocsArr; } - get byTools(): DocumentCollection[] { - const tileTypeToDocumentsMap = createTileTypeToDocumentsMap(this.metaDataDocs); + 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: DocumentCollection = { + const section: DocumentGroup = new DocumentGroup({ label: tileType, documents: tileTypeToDocumentsMap.get(tileType)?.documents ?? [], - }; + stores: this.stores + }); if (tileType === "Sparrow") { section.icon = SparrowHeaderIcon; } else { @@ -146,9 +146,9 @@ export class DocumentGroup { return sortedByLabel; } - get byBookmarked(): DocumentCollection[] { - const documentMap = createDocMapByBookmarks(this.metaDataDocs, this.stores.bookmarks); + get byBookmarked(): DocumentGroup[] { + const docMap = createDocMapByBookmarks(this.documents, this.stores.bookmarks); const sortedSectionLabels = ["Bookmarked", "Not Bookmarked"]; - return this.buildDocumentCollection(sortedSectionLabels, documentMap); + return this.buildDocumentCollection(sortedSectionLabels, docMap); } } diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index c3a7e5efa1..1effccef5c 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -168,11 +168,11 @@ describe('Sorted Documents Model', () => { const sortedDocsByGroup = sortedDocuments.byGroup; expect(sortedDocsByGroup.length).toBe(3); const group3 = sortedDocsByGroup.find(group => group.label === 'Group 3'); - expect(group3?.metaDataDocs.length).toBe(2); // Group 3 - Kirk + Scott + expect(group3?.documents.length).toBe(2); // Group 3 - Kirk + Scott const group5 = sortedDocsByGroup.find(group => group.label === 'Group 5'); - expect(group5?.metaDataDocs.length).toBe(1); // Group 5 - Joe + expect(group5?.documents.length).toBe(1); // Group 5 - Joe const group9 = sortedDocsByGroup.find(group => group.label === 'Group 9'); - expect(group9?.metaDataDocs.length).toBe(1); // Group 9 - Dennis + expect(group9?.documents.length).toBe(1); // Group 9 - Dennis }); it('should sort the groups numerically from least to greatest', () => { @@ -201,7 +201,7 @@ describe('Sorted Documents Model', () => { const sortedDocsByTools = sortedDocuments.byTools; const summaryOfResult = sortedDocsByTools.map(section => ({ sectionLabel: section.label, - docKeys: section.metaDataDocs.map(doc => doc.key) + docKeys: section.documents.map(doc => doc.key) })); expect(summaryOfResult).toEqual([ { sectionLabel: "Sketch", docKeys: [ diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 39cc447375..a4be8f0840 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -15,7 +15,6 @@ import { createDocMapByNames, createTileTypeToDocumentsMap, getTagsWithDocs, - DocumentCollection, sortGroupSectionLabels, sortNameSectionLabels } from "../../utilities/sort-document-utils"; @@ -23,8 +22,7 @@ import { DocumentGroup } from "./document-group"; import { getTileContentInfo } from "../tiles/tile-content-info"; import { PrimarySortType } from "./ui-types"; - -export type SortedDocumentsMap = Record; +export type SortedDocumentsMap = Record; export type TagWithDocs = { tagKey: string; @@ -101,14 +99,14 @@ export class SortedDocuments { 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, metaDataDocs: documentMap.get(label) ?? [] }); + return new DocumentGroup({stores: this.stores, label, documents: documentMap.get(label) ?? [] }); }); } 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, metaDataDocs: documentMap.get(label) ?? [] }); + return new DocumentGroup({ stores: this.stores, label, documents: documentMap.get(label) ?? [] }); }); } @@ -122,7 +120,7 @@ export class SortedDocuments { const label = tagWithDocs.tagValue; const docKeys = tagWithDocs.docKeysFoundWithTag; const documents = this.firestoreMetadataDocs.filter((doc: IDocumentMetadata) => docKeys.includes(doc.key)); - sortedDocsArr.push(new DocumentGroup({ stores: this.stores, label, metaDataDocs: documents })); + sortedDocsArr.push(new DocumentGroup({ stores: this.stores, label, documents })); }); return sortedDocsArr; } @@ -136,7 +134,7 @@ export class SortedDocuments { 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, metaDataDocs: documents, icon }); + const section = new DocumentGroup({ stores: this.stores, label, documents, icon }); return section; }); @@ -158,7 +156,7 @@ export class SortedDocuments { .map(label => new DocumentGroup({ stores: this.stores, label, - metaDataDocs: documentMap.get(label) ?? [] + documents: documentMap.get(label) ?? [] })); } From 68ef5af9f9b8009adc21e560a03675c4f97d2a36 Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 1 Aug 2024 15:54:26 -0400 Subject: [PATCH 025/127] Refactor code into use-sync-mst-node-to-firebase. --- .../document/editable-document-content.tsx | 36 +------- .../decorated-document-thumbnail-item.tsx | 2 +- src/hooks/use-document-sync-to-firebase.ts | 87 ++++++++++++++++--- 3 files changed, 78 insertions(+), 47 deletions(-) diff --git a/src/components/document/editable-document-content.tsx b/src/components/document/editable-document-content.tsx index 9f98a702a8..5b2ec0dd98 100644 --- a/src/components/document/editable-document-content.tsx +++ b/src/components/document/editable-document-content.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useContext, useRef, useState } from "react"; import classNames from "classnames"; -import { clone, onSnapshot } from "mobx-state-tree"; +import { clone } from "mobx-state-tree"; import { AppConfigContext } from "../../app-config-context"; import { CanvasComponent } from "./canvas"; import { DocumentContextReact } from "./document-context"; @@ -10,11 +10,10 @@ import { useDocumentSyncToFirebase } from "../../hooks/use-document-sync-to-fire import { useGroupsStore, useStores } from "../../hooks/use-stores"; import { ToolbarComponent } from "../toolbar"; import { EditableTileApiInterfaceRef, EditableTileApiInterfaceRefContext } from "../tiles/tile-api"; -import { DocumentModelSnapshotType, DocumentModelType } from "../../models/document/document"; +import { DocumentModelType } from "../../models/document/document"; import { ProblemDocument } from "../../models/document/document-types"; import { IToolbarModel } from "../../models/stores/problem-configuration"; import { WorkspaceMode } from "../../models/stores/workspace"; -import { ITileMapEntry } from "../../../functions/src/shared"; import "./editable-document-content.scss"; @@ -115,34 +114,7 @@ export function EditableDocumentContent({ contained ? "contained-editable-document-content" : "full-screen-editable-document-content", {"comment-select" : documentSelectedForComment, "full-height": fullHeight}, className); - useEffect(() => { - const disposer = onSnapshot(document, (snapshot: DocumentModelSnapshotType) => { - const tileMap = snapshot.content?.tileMap || {}; - const tileTypes: string[] = []; - - Object.keys(tileMap).forEach((key) => { - const tileInfo = tileMap[key] as ITileMapEntry; - const type = tileInfo.content.type; - if (!tileTypes.includes(type)) { - tileTypes.push(type); - } - }); - - // update tiletypes for metadata document in firestore - const query = firestore.collection("documents").where("key", "==", document.key); - query.get().then((querySnapshot) => { - querySnapshot.forEach((doc) => { - const docRef = doc.ref; - docRef.update({ - tileTypes, - }); - }); - }); - }); - return () => disposer(); - }, [document, firestore]); - - useDocumentSyncToFirebase(user, firebase, document, readOnly); + useDocumentSyncToFirebase(user, firebase, firestore, document, readOnly); return ( 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/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 25aaf28dc7..614d907058 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -1,4 +1,5 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; +import { throttle as _throttle } from "lodash"; 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 +9,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 { onSnapshot, SnapshotOut } from "@concord-consortium/mobx-state-tree"; +import { ITileMapEntry } from "functions/src/shared"; +import { DocumentContentSnapshotType } from "src/models/document/document-content"; function debugLog(...args: any[]) { // eslint-disable-next-line no-console @@ -25,7 +31,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); @@ -139,19 +150,67 @@ 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 tileTypes: string[] = []; + + Object.keys(tileMap).forEach((tileKey) => { + const tileInfo = tileMap[tileKey] as ITileMapEntry; + const tileType = tileInfo.content.type; + if (!tileTypes.includes(tileType)) { + tileTypes.push(tileType); + } + }); + + const promises = []; + + // update tiletypes for metadata document in firestore + const query = firestore.collection("documents").where("key", "==", document.key); + promises.push(query.get().then((querySnapshot) => { + return Promise.all( + querySnapshot.docs.map((doc) => { + const docRef = doc.ref; + return docRef.update({ + tileTypes, + }); + }) + ); + })); + + promises.push(firebase.ref(contentPath).update(transform?.(snapshot) ?? snapshot)); + return Promise.all(promises); + }, options); + + const throttledMutate = useMemo(() => _throttle(mutation.mutate, 1000), [mutation.mutate, 1000]); + + + 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 && From 56dfe17f717de403efad8d552d0e9710dfe5401f Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Sun, 4 Aug 2024 15:57:43 -0400 Subject: [PATCH 026/127] chore: code review improvements --- .../functional/teacher_tests/teacher_sort_work_view_spec.js | 4 ++-- src/components/document/sort-work-view.tsx | 1 + src/components/document/sorted-section.tsx | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) 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 f48773d4c2..9fe43c7f19 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 @@ -129,11 +129,11 @@ describe('SortWorkView Tests', () => { 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.wait(500); + cy.wait(500); // allow time for the scroll to complete 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.wait(500); + cy.wait(500); // allow time for the scroll to complete 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); diff --git a/src/components/document/sort-work-view.tsx b/src/components/document/sort-work-view.tsx index b077f9ab32..a5716f2ba8 100644 --- a/src/components/document/sort-work-view.tsx +++ b/src/components/document/sort-work-view.tsx @@ -13,6 +13,7 @@ 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. diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index c5c75a3a64..9670e359ac 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -14,9 +14,6 @@ import { ENavTab } from "../../models/view/nav-tabs"; import ArrowIcon from "../../assets/icons/arrow/arrow.svg"; -// TODO: Figure out how to totally move sorted-section-specific styles out of sort-work-view.scss without -// breaking the layout. The focus document header and close button are being affected. -import "./sort-work-view.scss"; import "./sorted-section.scss"; interface IProps { From 468fa3d6e656d16e014773d308709e9cca90a10d Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 5 Aug 2024 17:45:15 -0400 Subject: [PATCH 027/127] chore: remove unnecessary waits --- .../e2e/functional/teacher_tests/teacher_sort_work_view_spec.js | 2 -- 1 file changed, 2 deletions(-) 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 9fe43c7f19..2b4c20ba5c 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 @@ -129,11 +129,9 @@ describe('SortWorkView Tests', () => { 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.wait(500); // allow time for the scroll to complete 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.wait(500); // allow time for the scroll to complete 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); From 554e2435e29444bef0de1d4656d9bd02661fe670 Mon Sep 17 00:00:00 2001 From: lublagg Date: Thu, 8 Aug 2024 11:27:30 -0400 Subject: [PATCH 028/127] Add mock firestore to hook test. --- .../use-document-sync-to-firebase.test.ts | 101 +++++++++++------- src/hooks/use-document-sync-to-firebase.ts | 7 +- 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/hooks/use-document-sync-to-firebase.test.ts b/src/hooks/use-document-sync-to-firebase.test.ts index 74cf9c488f..dc911b9e3d 100644 --- a/src/hooks/use-document-sync-to-firebase.test.ts +++ b/src/hooks/use-document-sync-to-firebase.test.ts @@ -1,8 +1,10 @@ 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 "../lib/firebase"; +import { Firestore } from "../lib/firestore"; import { DocumentModel, createDocumentModel } from "../models/document/document"; import { LearningLogDocument, PersonalDocument, PlanningDocument, ProblemDocument @@ -52,11 +54,38 @@ const mockHttpsCallable = jest.fn((fn: string) => { return mockPostDocumentComment_v1; } }); -jest.mock("firebase/app", () => ({ - functions: () => ({ - httpsCallable: (fn: string) => mockHttpsCallable(fn) - }) -})); + +jest.mock("firebase/app", () => { + const mockInitializeApp = jest.fn(); + const mockFirestore = jest.fn().mockReturnValue({ + collection: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + get: jest.fn().mockResolvedValue({ + docs: [{ ref: { update: jest.fn().mockResolvedValue(undefined) } }] + }) + }) + }) + }); + return { + initializeApp: mockInitializeApp, + firestore: mockFirestore, + functions: () => ({ + httpsCallable: (fn: string) => mockHttpsCallable(fn) + }), + }; +}); + +const firebaseConfig = { + apiKey: "fake-api-key", + authDomain: "fake-auth-domain", + projectId: "fake-project-id", + storageBucket: "fake-storage-bucket", + messagingSenderId: "fake-messaging-sender-id", + appId: "fake-app-id" +}; + +import firebase from "firebase/app"; +firebase.initializeApp(firebaseConfig); const mockUpdate = jest.fn(); const mockRef = jest.fn(); @@ -78,6 +107,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 +122,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 +189,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 +216,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 +245,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 +273,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 +303,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 +334,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,18 +399,15 @@ 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); @@ -437,9 +465,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 +486,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 +545,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 +604,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 614d907058..47aed66ee1 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -198,8 +198,7 @@ export function useDocumentSyncToFirebase( return Promise.all(promises); }, options); - const throttledMutate = useMemo(() => _throttle(mutation.mutate, 1000), [mutation.mutate, 1000]); - + const throttledMutate = useMemo(() => _throttle(mutation.mutate, 1000), [mutation.mutate]); useEffect(() => { const cleanup = enabled @@ -209,7 +208,9 @@ export function useDocumentSyncToFirebase( throttledMutate(snapshot); }) : undefined; - return () => cleanup?.(); + return () => { + cleanup?.(); + }; }, [enabled, document.content, mutation, throttledMutate]); useEffect(() => { From c68618488e9f03d15b2aea05fd267abcc00b47af Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Thu, 8 Aug 2024 14:27:18 -0400 Subject: [PATCH 029/127] Add exemplar metadata to result of the sort-work query --- src/models/document/base-document-content.ts | 3 +++ src/models/stores/create-exemplar-docs.ts | 16 +++++++++++++-- src/models/stores/documents.ts | 4 ++++ src/models/stores/sorted-documents.ts | 21 ++++++++++++++++++++ src/models/stores/stores.ts | 2 ++ 5 files changed, 44 insertions(+), 2 deletions(-) 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/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/documents.ts b/src/models/stores/documents.ts index 80451fa8e9..e443647af1 100644 --- a/src/models/stores/documents.ts +++ b/src/models/stores/documents.ts @@ -170,6 +170,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/sorted-documents.ts b/src/models/stores/sorted-documents.ts index a4be8f0840..4ddec10062 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -192,6 +192,27 @@ export class SortedDocuments { 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 metadata: IDocumentMetadata = { + uid: doc.uid, + type: doc.type, + key: doc.key, + createdAt: doc.createdAt, + title: doc.title, + properties: undefined, + tileTypes: Array.from(doc.content?.tileTypes || []), + strategies: exemplarStrategy ? [exemplarStrategy] : [], + investigation: doc.investigation, + problem: doc.problem, + unit: doc.unit + }; + docsArray.push(metadata); + }); + runInAction(() => { this.firestoreMetadataDocs.replace(docsArray); }); diff --git a/src/models/stores/stores.ts b/src/models/stores/stores.ts index d1da65702f..75c464eb30 100644 --- a/src/models/stores/stores.ts +++ b/src/models/stores/stores.ts @@ -269,7 +269,9 @@ class Stores implements IStores{ }); showLoadingMessage("Loading exemplar documents"); createAndLoadExemplarDocs({ + unit, unitUrl: unitUrls.content, + investigation, problem, documents: this.documents, user: this.user, From 6d6bc75144fe8fb8f18b51cbd9b7c6f51d69d555 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Thu, 8 Aug 2024 16:23:37 -0400 Subject: [PATCH 030/127] Add tests, reinstate old exemplar tests. --- .../document_tests/exemplar_test_spec.js | 40 +++++++++++++++++-- cypress/support/elements/common/SortedWork.js | 16 ++++++++ .../problem-1/exemplar-1/content.json | 2 +- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/functional/document_tests/exemplar_test_spec.js b/cypress/e2e/functional/document_tests/exemplar_test_spec.js index a68a3bfb31..5ab50c4139 100644 --- a/cypress/e2e/functional/document_tests/exemplar_test_spec.js +++ b/cypress/e2e/functional/document_tests/exemplar_test_spec.js @@ -34,9 +34,8 @@ function addText(x, y, text) { drawToolTile.getTextDrawing().get('textarea').type(text + "{enter}"); } - // TODO: Reinstate the tests below when all metadata documents have the new fields and are updated in real time. - context.skip('Exemplar Documents', function () { - it('Unit with default config does not reveal exemplars or generate sticky notes', function () { + context('Exemplar Documents', function () { + it('Unit with default config does not initially hide exemplars or generate sticky notes', function () { beforeTest(queryParams2); cy.openTopTab('sort-work'); sortWork.openSortWorkSection("No Group"); @@ -65,6 +64,41 @@ function addText(x, y, text) { 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/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index b7bdb034bd..8c7ca665fa 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -11,6 +11,12 @@ class SortedWork { 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,10 @@ 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'); } @@ -65,6 +75,12 @@ 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); + } 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/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": [ { From ea1c29e554073615f2c65f8835b25f7dc5ab13f3 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Fri, 9 Aug 2024 12:00:46 -0400 Subject: [PATCH 031/127] remove unneeded initialization, and fix wrong mobx-state-tree import --- src/hooks/use-document-sync-to-firebase.test.ts | 15 +-------------- src/hooks/use-document-sync-to-firebase.ts | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/hooks/use-document-sync-to-firebase.test.ts b/src/hooks/use-document-sync-to-firebase.test.ts index dc911b9e3d..385cee1691 100644 --- a/src/hooks/use-document-sync-to-firebase.test.ts +++ b/src/hooks/use-document-sync-to-firebase.test.ts @@ -3,6 +3,7 @@ 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"; @@ -56,7 +57,6 @@ const mockHttpsCallable = jest.fn((fn: string) => { }); jest.mock("firebase/app", () => { - const mockInitializeApp = jest.fn(); const mockFirestore = jest.fn().mockReturnValue({ collection: jest.fn().mockReturnValue({ where: jest.fn().mockReturnValue({ @@ -67,7 +67,6 @@ jest.mock("firebase/app", () => { }) }); return { - initializeApp: mockInitializeApp, firestore: mockFirestore, functions: () => ({ httpsCallable: (fn: string) => mockHttpsCallable(fn) @@ -75,18 +74,6 @@ jest.mock("firebase/app", () => { }; }); -const firebaseConfig = { - apiKey: "fake-api-key", - authDomain: "fake-auth-domain", - projectId: "fake-project-id", - storageBucket: "fake-storage-bucket", - messagingSenderId: "fake-messaging-sender-id", - appId: "fake-app-id" -}; - -import firebase from "firebase/app"; -firebase.initializeApp(firebaseConfig); - const mockUpdate = jest.fn(); const mockRef = jest.fn(); diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 47aed66ee1..ff8274f609 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -1,5 +1,6 @@ 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"; @@ -11,7 +12,6 @@ import { isPublishedType, LearningLogDocument, LearningLogPublication, PersonalD import { UserModelType } from "../models/stores/user"; import { Firestore } from "src/lib/firestore"; import { useMutation, UseMutationOptions } from "react-query"; -import { onSnapshot, SnapshotOut } from "@concord-consortium/mobx-state-tree"; import { ITileMapEntry } from "functions/src/shared"; import { DocumentContentSnapshotType } from "src/models/document/document-content"; From 055d7445712f1a2dcf0c6e7283a94b5025561564 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 12 Aug 2024 10:39:18 -0400 Subject: [PATCH 032/127] Add some documentation --- README.md | 33 ++++++++++----- docs/firestore-schema.md | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 docs/firestore-schema.md diff --git a/README.md b/README.md index 40174c2a08..fbf1d46a74 100644 --- a/README.md +++ b/README.md @@ -140,28 +140,41 @@ $ npm run start:secure ## Testing/Deploying database rules -### Requirements: +### Requirements - * You should install the firebase CLI via: `npm install -g firebase-tools` - * You should be logged in to firebase: `firebase login` +- 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` + +Java is also required for running the emulators. There are various ways to install it; I did this: + +```shell +brew install java +echo 'export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"' >> ~/.zshrc +``` 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 diff --git a/docs/firestore-schema.md b/docs/firestore-schema.md new file mode 100644 index 0000000000..e7cf714c46 --- /dev/null +++ b/docs/firestore-schema.md @@ -0,0 +1,89 @@ +# Firestore database structure + +## Top level collections + +These are similar to Firebase. + +`authed, demo, dev, qa, tests, users` + +Within `authed`, various portal URLs: + +- learn-migrate_concord_org (has only `documents`) +- learn_concord_org +- learn_portal_staging_concord_org +- learn_staging_concord_org + +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/{classId}` + +Fields: + +- id (string, eg "60349") +- 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) _this needs updating_ + +### Contents of `curriculum/{docPath}` + +TODO + +### Contents of `documents/{docId}` + +Fields: + +- key: (string, a mobx id) +- title: (string) +- type: (string, eg "problem") +- uid: (string, id of user who owns this document) +- contextId: (always "Ignored"?) +- context_id: (string, uuid, should match context_id of a class) +- createdAt: (timestamp) +- network: (string, name of a network) +- originDoc: ? +- properties: (map, eg { pubCount: 1 }) +- teachers: (array of user IDs) _should be removed_ + +Collection: + +- comments + +#### 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 From 95fd0e5b7fdeacfe5533fe04b6a033d99ea285db Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Sun, 4 Aug 2024 14:26:00 -0400 Subject: [PATCH 033/127] feat: Filter all retains shared state (PT-188001508) [#188001508](https://www.pivotaltracker.com/story/show/188001508) --- functions/src/shared.ts | 1 + .../thumbnail/simple-document-item.scss | 15 ++++++++++- .../thumbnail/simple-document-item.tsx | 10 +++---- src/models/document/document-utils.ts | 27 +++++++++++++++++-- src/models/document/document.test.ts | 3 ++- src/models/document/document.ts | 26 ++++++------------ 6 files changed, 55 insertions(+), 27 deletions(-) diff --git a/functions/src/shared.ts b/functions/src/shared.ts index 599f02737e..5d4bc624c6 100644 --- a/functions/src/shared.ts +++ b/functions/src/shared.ts @@ -113,6 +113,7 @@ export interface IDocumentMetadata { investigation?: string; problem?: string; unit?: string; + visibility?: string; } export function isDocumentMetadata(o: any): o is IDocumentMetadata { return !!o.uid && !!o.type && !!o.key; diff --git a/src/components/thumbnail/simple-document-item.scss b/src/components/thumbnail/simple-document-item.scss index 0274703d37..4e09572358 100644 --- a/src/components/thumbnail/simple-document-item.scss +++ b/src/components/thumbnail/simple-document-item.scss @@ -2,7 +2,7 @@ .simple-document-item { background: $classwork-purple-light-7; - border: solid 1px #707070; + border: solid 1px $charcoal; border-radius: 1px; cursor: pointer; display: flex; @@ -18,4 +18,17 @@ &: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 index d1db754074..4e2c60b71e 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -1,6 +1,7 @@ import React from "react"; import { IDocumentMetadata } from "../../../functions/src/shared"; import { useStores } from "../../hooks/use-stores"; +import { isDocumentAccessibleToUser } from "../../models/document/document-utils"; import "./simple-document-item.scss"; @@ -12,7 +13,7 @@ interface IProps { } export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDocument, problemOrdinal }: IProps) => { - const { class: classStore, unit } = useStores(); + const { documents, class: classStore, unit, user } = useStores(); const { uid } = document; const userName = classStore.getUserById(uid)?.displayName; const investigations = unit.investigations; @@ -22,8 +23,7 @@ export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDoc const investigation = investigations[Number(investigationOrdinal)]; const problem = investigation?.problems[Number(problemOrdinal) - 1]; const title = document.title ? `${userName}: ${document.title}` : `${userName}: ${problem?.title ?? "unknown title"}`; - // TODO: Account for and use isPrivate in the view. isAccessibleToUser won't currently work here. - // const isPrivate = !document.isAccessibleToUser(user, documents); + const isPrivate = !isDocumentAccessibleToUser(document, user, documents); const handleClick = () => { onSelectDocument(document); @@ -31,10 +31,10 @@ export const SimpleDocumentItem = ({ document, investigationOrdinal, onSelectDoc return (
); diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index adfb1ee6f6..3e920a9e73 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -1,11 +1,14 @@ import { getParent } from "mobx-state-tree"; +import { IDocumentMetadata } from "../../../functions/src/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, LearningLogPublication, PersonalPublication, + ProblemPublication, SupportPublication } from "./document-types"; export function getDocumentDisplayTitle( document: DocumentModelType, appConfig: AppConfigModelType, problem?: ProblemModelType, @@ -44,3 +47,23 @@ export function getDocumentIdentifier(document?: DocumentContentModelType) { return getSectionPath(section); } } + +export const isDocumentPublished = (doc: IDocumentMetadata) => { + return (doc.type === ProblemPublication) + || (doc.type === LearningLogPublication) + || (doc.type === PersonalPublication) + || (doc.type === SupportPublication); +}; + +export const isDocumentAccessibleToUser = ( + doc: IDocumentMetadata, user: UserModelType, documentStore: IExemplarVisibilityProvider +) => { + const ownDocument = doc.uid === user.id; + const isShared = doc.visibility === "public"; + if (user.type === "teacher") return true; + if (user.type === "student") { + return ownDocument || isShared || isDocumentPublished(doc) + || (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 9cec2fe16c..ef0031c0b4 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -4,7 +4,6 @@ import { QueryClient, UseQueryResult } from "react-query"; import { DocumentContentModel, DocumentContentSnapshotType } from "./document-content"; import { IDocumentAddTileOptions } from "./document-content-types"; import { DocumentTypeEnum, IDocumentContext, ISetProperties, - isExemplarType, LearningLogDocument, LearningLogPublication, PersonalDocument, PersonalPublication, PlanningDocument, ProblemDocument, ProblemPublication, SupportPublication } from "./document-types"; @@ -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, isDocumentPublished } from "./document-utils"; export enum ContentStatus { Valid, Error } -type IExemplarVisibilityProvider = { +export type IExemplarVisibilityProvider = { isExemplarVisible: (id: string) => boolean; }; @@ -86,12 +86,6 @@ export const DocumentModel = Tree.named("Document") get isSupport() { return self.type === SupportPublication; }, - get isPublished() { - return (self.type === ProblemPublication) - || (self.type === LearningLogPublication) - || (self.type === PersonalPublication) - || (self.type === SupportPublication); - }, get isRemote() { return !!self.remoteContext; }, @@ -104,14 +98,14 @@ 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(), visibility } as IDocumentMetadata; }, getProperty(key: string) { return self.properties.get(key); @@ -125,6 +119,9 @@ export const DocumentModel = Tree.named("Document") }, })) .views(self => ({ + get isPublished() { + return isDocumentPublished(self.metadata); + }, getLabel(appConfig: AppConfigModelType, count: number, lowerCase?: boolean) { const props = appConfig.documentLabelProperties || []; let docStr = self.type as string; @@ -160,14 +157,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) => ({ From fede16bb29b0af311d7b314e96f9a78eedefebc2 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 12 Aug 2024 12:51:30 -0400 Subject: [PATCH 034/127] chore: use `isPublishedType()` --- src/models/document/document-utils.ts | 13 +++---------- src/models/document/document.ts | 10 +++++----- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index 3e920a9e73..59257ce4d6 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -7,8 +7,7 @@ import { AppConfigModelType } from "../stores/app-config-model"; import { UserModelType } from "../stores/user"; import { DocumentModelType, IExemplarVisibilityProvider } from "./document"; import { DocumentContentModelType } from "./document-content"; -import { isExemplarType, isPlanningType, isProblemType, LearningLogPublication, PersonalPublication, - ProblemPublication, SupportPublication } from "./document-types"; +import { isExemplarType, isPlanningType, isProblemType, isPublishedType } from "./document-types"; export function getDocumentDisplayTitle( document: DocumentModelType, appConfig: AppConfigModelType, problem?: ProblemModelType, @@ -48,21 +47,15 @@ export function getDocumentIdentifier(document?: DocumentContentModelType) { } } -export const isDocumentPublished = (doc: IDocumentMetadata) => { - return (doc.type === ProblemPublication) - || (doc.type === LearningLogPublication) - || (doc.type === PersonalPublication) - || (doc.type === SupportPublication); -}; - 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 || isDocumentPublished(doc) + return ownDocument || isShared || isPublished || (isExemplarType(doc.type) && documentStore.isExemplarVisible(doc.key)); } return false; diff --git a/src/models/document/document.ts b/src/models/document/document.ts index ef0031c0b4..bf7236cee0 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -3,7 +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, +import { DocumentTypeEnum, IDocumentContext, ISetProperties, isPublishedType, LearningLogDocument, LearningLogPublication, PersonalDocument, PersonalPublication, PlanningDocument, ProblemDocument, ProblemPublication, SupportPublication } from "./document-types"; @@ -26,7 +26,7 @@ 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, isDocumentPublished } from "./document-utils"; +import { isDocumentAccessibleToUser } from "./document-utils"; export enum ContentStatus { Valid, @@ -86,6 +86,9 @@ export const DocumentModel = Tree.named("Document") get isSupport() { return self.type === SupportPublication; }, + get isPublished() { + return isPublishedType(self.type); + }, get isRemote() { return !!self.remoteContext; }, @@ -119,9 +122,6 @@ export const DocumentModel = Tree.named("Document") }, })) .views(self => ({ - get isPublished() { - return isDocumentPublished(self.metadata); - }, getLabel(appConfig: AppConfigModelType, count: number, lowerCase?: boolean) { const props = appConfig.documentLabelProperties || []; let docStr = self.type as string; From 360c49b885705139cc5a38a0f9b7b52da66a4c54 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 12 Aug 2024 16:26:07 -0400 Subject: [PATCH 035/127] chore: add tests --- cypress.config.ts | 1 + .../document_tests/student_test_spec.js | 29 +++++++++++++++++-- cypress/support/elements/common/SortedWork.js | 20 +++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) 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/functional/document_tests/student_test_spec.js b/cypress/e2e/functional/document_tests/student_test_spec.js index c866335045..b2f3b16a35 100644 --- a/cypress/e2e/functional/document_tests/student_test_spec.js +++ b/cypress/e2e/functional/document_tests/student_test_spec.js @@ -1,16 +1,17 @@ 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")}`; +function beforeTest(queryParams) { cy.clearQAData('all'); cy.visit(queryParams); cy.waitForLoad(); @@ -18,7 +19,7 @@ function beforeTest() { 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 +44,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/support/elements/common/SortedWork.js b/cypress/support/elements/common/SortedWork.js index 8c7ca665fa..dd02b32ed8 100644 --- a/cypress/support/elements/common/SortedWork.js +++ b/cypress/support/elements/common/SortedWork.js @@ -81,6 +81,26 @@ class SortedWork { 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'); From b9999168aec0cc93ff013052ad36731abcc91f81 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 13 Aug 2024 09:59:22 -0400 Subject: [PATCH 036/127] Look up teachers in class records, using context id as the id --- docs/firestore-schema.md | 10 +++---- firebase-test/src/documents-rules.test.ts | 27 +++++++++++++++-- firestore.rules | 36 +++++++++++++++-------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/docs/firestore-schema.md b/docs/firestore-schema.md index e7cf714c46..5c861e1fa7 100644 --- a/docs/firestore-schema.md +++ b/docs/firestore-schema.md @@ -6,12 +6,12 @@ These are similar to Firebase. `authed, demo, dev, qa, tests, users` -Within `authed`, various portal URLs: +Within `authed`, there are several portal site names: -- learn-migrate_concord_org (has only `documents`) -- learn_concord_org -- learn_portal_staging_concord_org -- learn_staging_concord_org +- `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") diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index 86573ee07f..dabd27b7ed 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(); @@ -129,15 +143,24 @@ describe("Firestore security rules", () => { 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 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" }); + }); + it("authenticated teachers can't update user documents' read-only fields", async () => { db = initFirestore(teacherAuth); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); diff --git a/firestore.rules b/firestore.rules index 820616ea69..32bc4ec061 100644 --- a/firestore.rules +++ b/firestore.rules @@ -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; @@ -93,8 +88,9 @@ service cloud.firestore { } function isValidDocumentCreateRequest() { - return userInRequestTeachers() && classIsRequestContextId() && - request.resource.data.keys().hasAll(["uid", "network", "type", "key", "createdAt"]); + return + classIsRequestContextId() && + request.resource.data.keys().hasAll(["uid", "network", "type", "key", "createdAt"]); } function preservesReadOnlyDocumentFields() { @@ -103,10 +99,6 @@ service cloud.firestore { return !affectedFieldsSet.hasAny(readOnlyFieldsSet); } - function isValidDocumentUpdateRequest() { - return userInResourceTeachers() && preservesReadOnlyDocumentFields(); - } - function isValidSupportCreateRequest() { return userIsRequestUser() && classInRequestClasses() && @@ -132,6 +124,23 @@ service cloud.firestore { match /authed/{portal} { allow read, write: if isAuthedTeacher(); + // 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() { + let is_authed = isAuthed(); + let contextid = resource.data.context_id; + let resource_class = get(/databases/$(database)/documents/authed/$(portal)/classes/$(contextid)).data; + let teacher_in_class = resource_class!=null && string(request.auth.token.platform_user_id) in resource_class.teachers; + return + (is_authed && teacher_in_class) + || (is_authed && string(request.auth.token.platform_user_id) in resource.data.teachers) + ; + } + + function isValidDocumentUpdateRequest() { + return userInResourceTeachers() && preservesReadOnlyDocumentFields(); + } + // return list of networks available to the current teacher function getTeacherNetworks() { let platformUserId = string(request.auth.token.platform_user_id); @@ -331,7 +340,8 @@ service cloud.firestore { 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 || + ('teachers' in docData && + (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 From b22417f12d528c90fbde8d383cd523bc298702df Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 13 Aug 2024 12:04:33 -0400 Subject: [PATCH 037/127] Allow commenting in teacher's secondary classes --- firebase-test/src/documents-rules.test.ts | 48 +++++++++++++++++++++++ firestore.rules | 30 ++++++++------ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index dabd27b7ed..1bff2a95ee 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -161,6 +161,37 @@ describe("Firestore security rules", () => { 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.skip("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.skip("authenticated teachers can update user documents in secondary class", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await specClassDoc(otherClass, teacherId); + await adminWriteDoc(kDocumentDocPath, ({ 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 fields", async () => { db = initFirestore(teacherAuth); await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); @@ -475,6 +506,14 @@ 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()); @@ -493,6 +532,15 @@ describe("Firestore security rules", () => { await expectUpdateToSucceed(db, kDocumentCommentDocPath, { content: "A new comment!" }); }); + 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!" }); + }); + it("authenticated teachers can't update other teachers' document comments", async () => { await initFirestoreWithUserDocument(teacher2Auth); await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); diff --git a/firestore.rules b/firestore.rules index 32bc4ec061..0b579c3c40 100644 --- a/firestore.rules +++ b/firestore.rules @@ -124,17 +124,18 @@ 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)).data; + return class_doc != null && string(request.auth.token.platform_user_id) in class_doc.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() { - let is_authed = isAuthed(); - let contextid = resource.data.context_id; - let resource_class = get(/databases/$(database)/documents/authed/$(portal)/classes/$(contextid)).data; - let teacher_in_class = resource_class!=null && string(request.auth.token.platform_user_id) in resource_class.teachers; - return - (is_authed && teacher_in_class) - || (is_authed && string(request.auth.token.platform_user_id) in resource.data.teachers) - ; + return isAuthed() && + ( teacherIsInClass(resource.data.context_id) || + string(request.auth.token.platform_user_id) in resource.data.teachers); } function isValidDocumentUpdateRequest() { @@ -339,13 +340,16 @@ 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 - ('teachers' in docData && - (string(request.auth.token.platform_user_id) in docData.teachers)) || + // check if document is in user's class + request.auth.token.class_hash == docData.context_id || // 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 + // 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)) ); } From a76c45f7fd2701b59e271ca0ca9db75437fdbcb6 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 13 Aug 2024 13:53:20 -0400 Subject: [PATCH 038/127] When user adds annotation, the type is added to tileTypes metadata field. --- src/hooks/use-document-sync-to-firebase.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index ff8274f609..67a4eb6b32 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -14,6 +14,7 @@ import { Firestore } from "src/lib/firestore"; import { useMutation, UseMutationOptions } from "react-query"; import { ITileMapEntry } from "functions/src/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 @@ -179,6 +180,20 @@ export function useDocumentSyncToFirebase( } }); + // 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) => { + const annotation = annotations[annotationKey]; + const annotationType = annotation.type; + if (!tileTypes.includes(annotationType)) { + tileTypes.push(annotationType); + } + }); + const promises = []; // update tiletypes for metadata document in firestore From b9c9fa13b763e86ea66a8df94046282399d200cf Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 13 Aug 2024 14:16:50 -0400 Subject: [PATCH 039/127] Rename tileType doc property to 'tools' + update metadata script to add annotations to 'tools' --- functions/src/shared.ts | 2 +- scripts/ai/update-metadata.ts | 21 +++++++++++++++------ src/hooks/use-document-sync-to-firebase.ts | 17 +++++++++-------- src/models/stores/sorted-documents.ts | 2 +- src/utilities/sort-document-utils.ts | 18 +++++++++--------- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/functions/src/shared.ts b/functions/src/shared.ts index 599f02737e..5563c8c7e3 100644 --- a/functions/src/shared.ts +++ b/functions/src/shared.ts @@ -108,7 +108,7 @@ export interface IDocumentMetadata { title?: string; originDoc?: string; properties?: Record; - tileTypes?: string[]; + tools?: string[]; strategies?: string[]; investigation?: string; problem?: string; diff --git a/scripts/ai/update-metadata.ts b/scripts/ai/update-metadata.ts index b57832fcd9..ece78dea90 100644 --- a/scripts/ai/update-metadata.ts +++ b/scripts/ai/update-metadata.ts @@ -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 || []; + 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 (!tools.includes(annotation.type) && annotation.type === "arrowAnnotation") { + tools.push("Sparrow"); } } @@ -198,7 +207,7 @@ async function processFile(file: string) { // 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, @@ -223,8 +232,8 @@ async function processFile(file: string) { 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({ strategies, tools } as any); + console.log(documentId, doc.id, "Updated metadata with", { strategies, tools }); doc.ref.update({ visibility } as any); console.log(documentId, doc.id, "Updated metadata with", { visibility }); metadataUpdated++; diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 67a4eb6b32..8adda116bc 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -170,13 +170,13 @@ export function useDocumentSyncToFirebase( const mutation = useMutation((snapshot: DocumentContentSnapshotType) => { const tileMap = snapshot.tileMap || {}; - const tileTypes: string[] = []; + const tools: string[] = []; Object.keys(tileMap).forEach((tileKey) => { const tileInfo = tileMap[tileKey] as ITileMapEntry; const tileType = tileInfo.content.type; - if (!tileTypes.includes(tileType)) { - tileTypes.push(tileType); + if (!tools.includes(tileType)) { + tools.push(tileType); } }); @@ -186,11 +186,12 @@ export function useDocumentSyncToFirebase( const annotations = (snapshot as {annotations: Record}).annotations || {}; - Object.keys(annotations).forEach((annotationKey) => { + Object.keys(annotations).forEach((annotationKey: string) => { const annotation = annotations[annotationKey]; - const annotationType = annotation.type; - if (!tileTypes.includes(annotationType)) { - tileTypes.push(annotationType); + // for now we only want Sparrow annotations + // we might want to change this if we want to count other types in the future + if (!tools.includes(annotation.type) && annotation.type === "arrowAnnotation") { + tools.push("Sparrow"); } }); @@ -203,7 +204,7 @@ export function useDocumentSyncToFirebase( querySnapshot.docs.map((doc) => { const docRef = doc.ref; return docRef.update({ - tileTypes, + tools, }); }) ); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 4ddec10062..001863a935 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -204,7 +204,7 @@ export class SortedDocuments { createdAt: doc.createdAt, title: doc.title, properties: undefined, - tileTypes: Array.from(doc.content?.tileTypes || []), + tools: Array.from(doc.content?.tileTypes || []), strategies: exemplarStrategy ? [exemplarStrategy] : [], investigation: doc.investigation, problem: doc.problem, diff --git a/src/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts index ee57c9bdbb..1b58f5b110 100644 --- a/src/utilities/sort-document-utils.ts +++ b/src/utilities/sort-document-utils.ts @@ -126,9 +126,9 @@ export const getTagsWithDocs = (documents: IDocumentMetadata[], commentTags: Rec }; export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => { - const tileTypeToDocumentsMap = new Map>(); + const toolToDocumentsMap = new Map>(); const addDocByType = (docToAdd: IDocumentMetadata, type: string) => { - if (!tileTypeToDocumentsMap.get(type)) { + if (!toolToDocumentsMap.get(type)) { let icon: FC> | undefined; if (type === "Sparrow") { icon = SparrowHeaderIcon; @@ -136,23 +136,23 @@ export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => const componentInfo = getTileComponentInfo(type); icon = componentInfo?.HeaderIcon; } - tileTypeToDocumentsMap.set(type, { + toolToDocumentsMap.set(type, { icon, documents: [] } ); } - tileTypeToDocumentsMap.get(type)?.documents.push(docToAdd); + 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.tileTypes) { - const validTileTypes = doc.tileTypes.filter(type => type !== "Placeholder" && type !== "Unknown"); + if (doc.tools) { + const validTileTypes = doc.tools.filter(type => type !== "Placeholder" && type !== "Unknown"); if (validTileTypes.length > 0) { - validTileTypes.forEach(tileType => { - addDocByType(doc, tileType); + validTileTypes.forEach(tool => { + addDocByType(doc, tool); }); // TODO: Sparrow annotations. We'll first need to add information about these to metadata docs. } else { @@ -161,7 +161,7 @@ export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => } }); - return tileTypeToDocumentsMap; + return toolToDocumentsMap; }; export const createDocMapByBookmarks = (documents: IDocumentMetadata[], bookmarks: Bookmarks) => { From f5ec4a39d750bab3f284e9c3ad3f9f41f94478a8 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 13 Aug 2024 14:24:21 -0400 Subject: [PATCH 040/127] Change mock metadata in specs to use 'tools' instead of 'tileTypes'. --- src/models/stores/document-group.test.ts | 8 ++++---- src/models/stores/sorted-documents.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/models/stores/document-group.test.ts b/src/models/stores/document-group.test.ts index 8c1ee1bf69..c98b4bc18c 100644 --- a/src/models/stores/document-group.test.ts +++ b/src/models/stores/document-group.test.ts @@ -40,23 +40,23 @@ const mockMetadataDocuments: IObservableArray = observable.ar type: ProblemDocument, key:"Student 1 Problem Doc Group 5", createdAt: 1, - tileTypes: [], + tools: [], strategies: ["foo", "bar"], }, { uid: "2", //Scott type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, - tileTypes: ["Text"] + tools: ["Text"] }, { uid: "3", //Dennis type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, - tileTypes: ["Drawing"] + tools: ["Drawing"] }, { uid: "4", //Kirk type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, - tileTypes: [], + tools: [], strategies: ["bar"] } ]); diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 1effccef5c..422b363128 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -37,22 +37,22 @@ const mockMetadataDocuments: IObservableArray = observable.ar { uid: "1", //Joe type: ProblemDocument, key:"Student 1 Problem Doc Group 5", createdAt: 1, - tileTypes: [] + tools: [] }, { uid: "2", //Scott type: ProblemDocument, key:"Student 2 Problem Doc Group 3", createdAt: 2, - tileTypes: ["Text"] + tools: ["Text"] }, { uid: "3", //Dennis type: ProblemDocument, key:"Student 3 Problem Doc Group 9", createdAt: 3, - tileTypes: ["Drawing"] + tools: ["Drawing"] }, { uid: "4", //Kirk type: ProblemDocument, key:"Student 4 Problem Doc Group 3", createdAt: 4, - tileTypes: [] + tools: [] } ]); From 1a4608c9cd016f019683aff73ba943a3a0681071 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Tue, 13 Aug 2024 16:20:42 -0400 Subject: [PATCH 041/127] Update storage of class documents --- src/lib/firestore-schema.ts | 4 +-- src/lib/teacher-network.test.ts | 26 ++++++++-------- src/lib/teacher-network.ts | 53 ++++++++++++++++++++------------- src/utilities/js-utils.ts | 10 +++++++ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/lib/firestore-schema.ts b/src/lib/firestore-schema.ts index 8359984435..11d8447323 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,7 @@ 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 } // collection key is `${network}_${context_id (class hash)}` type ClassesCollection = FSCollection; diff --git a/src/lib/teacher-network.test.ts b/src/lib/teacher-network.test.ts index b22299ba77..4a403c86eb 100644 --- a/src/lib/teacher-network.test.ts +++ b/src/lib/teacher-network.test.ts @@ -165,6 +165,7 @@ describe("Teacher network functions", () => { it("should do nothing if the class already exists", async () => { mockDocGet.mockImplementation(() => Promise.resolve(fsClass1)); + fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); const result = await syncClass(firestore, kPortalJWT, partClass1); expect(mockDoc).toHaveBeenCalledWith(classDocPath); @@ -179,8 +180,8 @@ describe("Teacher network functions", () => { 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(); + expect(mockDoc).not.toHaveBeenCalled(); + expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; }); @@ -190,8 +191,8 @@ describe("Teacher network functions", () => { fetchMock.mockRejectOnce(new Error()); const firestore = new Firestore(mockDB); const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); - expect(mockDocGet).toHaveBeenCalled(); + expect(mockDoc).not.toHaveBeenCalled(); + expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; }); @@ -309,23 +310,24 @@ describe("Teacher network functions", () => { expect(mockDocSet).not.toHaveBeenCalled(); }); - 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(); + expect(mockDoc).toHaveBeenCalledTimes(1); + expect(mockDocGet).toHaveBeenCalledTimes(1); + expect(mockDocSet).toHaveBeenCalledTimes(0); }); 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, 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..e1157ba29a 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -1,5 +1,6 @@ import { Optional } from "utility-types"; import { UserModelType } from "../models/stores/user"; +import { arraysEqualIgnoringOrder } from "../utilities/js-utils"; import { Firestore } from "./firestore"; import { ClassDocument, OfferingDocument } from "./firestore-schema"; import { IPortalClassInfo } from "./portal-types"; @@ -49,7 +50,6 @@ 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) { const { network } = user; - if (!network) return []; const promises: Promise[] = []; @@ -67,31 +67,44 @@ export function syncTeacherClassesAndOfferings(firestore: Firestore, user: UserM 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)); - }); + if (network) { + // 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); +} - return promises; +async function createOrUpdateClassDoc(firestore: Firestore, docPath: string, aClass: ClassDocument): + Promise { + return firestore.guaranteeDocument(docPath, + async () => { return aClass; }, + (content) => { return !content || !arraysEqualIgnoringOrder(aClass.teachers, content.teachers); } + ); } 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; - } - }); + const promises: Promise[] = []; + if (uri && context_id && rawPortalJWT) { + const teachers = await getClassTeachers(uri, rawPortalJWT); + if (!teachers) return; + const classWithTeachers = { ...aClass, teachers }; + // Old location of the class document + if (network) { + promises.push(createOrUpdateClassDoc(firestore, `classes/${network}_${context_id}`, classWithTeachers)); + } + // New location of the class document + promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers)); } + return Promise.all(promises); } export async function syncOffering( 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)); +} From 38393ac5276adad22fb46f769c6530a7dcbbe809 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 12 Aug 2024 18:25:51 -0400 Subject: [PATCH 042/127] feat: Show changes to sharing (PT-188065262) [#188065262](https://www.pivotaltracker.com/story/show/188065262) --- .../use-document-sync-to-firebase.test.ts | 9 ++++----- src/hooks/use-document-sync-to-firebase.ts | 19 +++++++++++++++++-- src/hooks/use-sync-mst-prop-to-firebase.ts | 11 +++++++++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/hooks/use-document-sync-to-firebase.test.ts b/src/hooks/use-document-sync-to-firebase.test.ts index 385cee1691..98d335acad 100644 --- a/src/hooks/use-document-sync-to-firebase.test.ts +++ b/src/hooks/use-document-sync-to-firebase.test.ts @@ -401,13 +401,12 @@ describe("useDocumentSyncToFirebase hook", () => { }); // 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(); diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index ff8274f609..65a644440d 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -75,6 +75,19 @@ export function useDocumentSyncToFirebase( const commonSyncEnabled = !disableFirebaseSync && contentStatus === ContentStatus.Valid; + const syncFirestoreDocumentProp = (prop: string, value?: string) => { + const promises = []; + const query = firestore.collection("documents").where("key", "==", document.key); + + promises.push(query.get().then((querySnapshot) => { + return Promise.all( + querySnapshot.docs.map((doc) => doc.ref.update({ [prop]: value})) + ); + })); + + return Promise.all(promises); + }; + // sync visibility (public/private) for problem documents useSyncMstPropToFirebase({ firebase, model: document, prop: "visibility", path: typedMetadata, @@ -86,7 +99,8 @@ export function useDocumentSyncToFirebase( onError: (err, visibility) => { console.warn(`ERROR: Failed to update document visibility for ${type} document ${key}:`, visibility); } - } + }, + additionalMutation: syncFirestoreDocumentProp }); // sync visibility (public/private) for personal and learning log documents @@ -100,7 +114,8 @@ export function useDocumentSyncToFirebase( onError: (err, visibility) => { console.warn(`ERROR: Failed to update document visibility for ${type} document ${key}:`, visibility); } - } + }, + additionalMutation: syncFirestoreDocumentProp }); // sync title for personal and learning log 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..6a645549c7 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,12 @@ export function useSyncMstPropToFirebase { const should = typeof shouldMutate === "function" ? shouldMutate(value) : shouldMutate; - return should ? firebase.ref(path).update({ [prop]: value }) : Promise.resolve(); + const mutations = Promise.all([ + should ? firebase.ref(path).update({ [prop]: value }) : Promise.resolve(), + additionalMutation ? additionalMutation(prop, value) : Promise.resolve() + ]); + + return mutations; }, options); const throttledMutate = useMemo(() => _throttle(mutation.mutate, throttle), [mutation.mutate, throttle]); From ec86ae1f7f18d33e6698e843006967430105ce4e Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 14 Aug 2024 10:13:17 -0400 Subject: [PATCH 043/127] Sync classes even if no teacher network. --- src/components/app.tsx | 7 +++---- src/lib/teacher-network.ts | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index 9a518c7c8d..e474101108 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -128,10 +128,9 @@ 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); - } + } + if (rawPortalJWT) { + syncTeacherClassesAndOfferings(db.firestore, user, rawPortalJWT); } }) .then(() => { diff --git a/src/lib/teacher-network.ts b/src/lib/teacher-network.ts index e1157ba29a..bdf84f9538 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -97,11 +97,19 @@ export async function syncClass(firestore: Firestore, rawPortalJWT: string, aCla const teachers = await getClassTeachers(uri, rawPortalJWT); if (!teachers) return; const classWithTeachers = { ...aClass, teachers }; + + // Firestore will not accept 'undefined' values + if (classWithTeachers.network === undefined) { + delete classWithTeachers.network; + } + // Old location of the class document if (network) { + console.log('attempting to set new class doc:', `classes/${network}_${context_id}`, classWithTeachers); promises.push(createOrUpdateClassDoc(firestore, `classes/${network}_${context_id}`, classWithTeachers)); } // New location of the class document + console.log('attempting to set new class doc:', `classes/${context_id}`, classWithTeachers); promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers)); } return Promise.all(promises); From ee740f9aeb8d60b59fcde4b9710d3aeca843c741 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 14 Aug 2024 10:39:59 -0400 Subject: [PATCH 044/127] Check if tools includes "Sparrow" + add annotations to exemplar docs. --- scripts/ai/update-metadata.ts | 2 +- src/hooks/use-document-sync-to-firebase.ts | 2 +- src/models/stores/sorted-documents.ts | 13 ++++++++++++- src/utilities/sort-document-utils.ts | 1 - 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/ai/update-metadata.ts b/scripts/ai/update-metadata.ts index ece78dea90..c7482c2e8d 100644 --- a/scripts/ai/update-metadata.ts +++ b/scripts/ai/update-metadata.ts @@ -109,7 +109,7 @@ async function processFile(file: string) { 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 (!tools.includes(annotation.type) && annotation.type === "arrowAnnotation") { + if (annotation.type === "arrowAnnotation" && !tools.includes("Sparrow")) { tools.push("Sparrow"); } } diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 8adda116bc..6ecce9b816 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -190,7 +190,7 @@ export function useDocumentSyncToFirebase( 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 (!tools.includes(annotation.type) && annotation.type === "arrowAnnotation") { + if (annotation.type === "arrowAnnotation" && !tools.includes("Sparrow")) { tools.push("Sparrow"); } }); diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 001863a935..6f387a708a 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -21,6 +21,7 @@ import { import { DocumentGroup } from "./document-group"; import { getTileContentInfo } from "../tiles/tile-content-info"; import { PrimarySortType } from "./ui-types"; +import { IArrowAnnotation } from "../annotations/arrow-annotation"; export type SortedDocumentsMap = Record; @@ -197,6 +198,16 @@ export class SortedDocuments { // 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, @@ -204,7 +215,7 @@ export class SortedDocuments { createdAt: doc.createdAt, title: doc.title, properties: undefined, - tools: Array.from(doc.content?.tileTypes || []), + tools, strategies: exemplarStrategy ? [exemplarStrategy] : [], investigation: doc.investigation, problem: doc.problem, diff --git a/src/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts index 1b58f5b110..d92f1d76b1 100644 --- a/src/utilities/sort-document-utils.ts +++ b/src/utilities/sort-document-utils.ts @@ -154,7 +154,6 @@ export const createTileTypeToDocumentsMap = (documents: IDocumentMetadata[]) => validTileTypes.forEach(tool => { addDocByType(doc, tool); }); - // TODO: Sparrow annotations. We'll first need to add information about these to metadata docs. } else { addDocByType(doc, "No Tools"); } From 337e9ca04790dda76f34a4743790f13628acdbbf Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Wed, 14 Aug 2024 15:10:57 -0400 Subject: [PATCH 045/127] chore: code review suggestions --- src/hooks/use-document-sync-to-firebase.ts | 7 ++----- src/hooks/use-sync-mst-prop-to-firebase.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 65a644440d..2c2a0cda14 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -76,16 +76,13 @@ export function useDocumentSyncToFirebase( const commonSyncEnabled = !disableFirebaseSync && contentStatus === ContentStatus.Valid; const syncFirestoreDocumentProp = (prop: string, value?: string) => { - const promises = []; const query = firestore.collection("documents").where("key", "==", document.key); - promises.push(query.get().then((querySnapshot) => { + return query.get().then((querySnapshot) => { return Promise.all( querySnapshot.docs.map((doc) => doc.ref.update({ [prop]: value})) ); - })); - - return Promise.all(promises); + }); }; // sync visibility (public/private) for problem documents diff --git a/src/hooks/use-sync-mst-prop-to-firebase.ts b/src/hooks/use-sync-mst-prop-to-firebase.ts index 6a645549c7..d6d1b428d0 100644 --- a/src/hooks/use-sync-mst-prop-to-firebase.ts +++ b/src/hooks/use-sync-mst-prop-to-firebase.ts @@ -37,10 +37,12 @@ export function useSyncMstPropToFirebase { const should = typeof shouldMutate === "function" ? shouldMutate(value) : shouldMutate; - const mutations = Promise.all([ - should ? firebase.ref(path).update({ [prop]: value }) : Promise.resolve(), - additionalMutation ? additionalMutation(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); From e5afac3b0b89f528cebcad7e0b09832f68c3b291 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 14 Aug 2024 15:36:14 -0400 Subject: [PATCH 046/127] Doc updates --- docs/firestore-schema.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/firestore-schema.md b/docs/firestore-schema.md index 5c861e1fa7..bd55a2d6be 100644 --- a/docs/firestore-schema.md +++ b/docs/firestore-schema.md @@ -38,17 +38,22 @@ Collections within `(authed|demo|dev|qa)/{id}`: ## Third level -### Contents of `classes/{classId}` +### Contents of `classes/{classDocId}` + +Currently we are storing class docs under both of these `classDocId`s: + +- classes/{network}_{contextid} +- classes/{contextid} Fields: -- id (string, eg "60349") +- 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) _this needs updating_ +- teachers: (array of IDs of teachers) ### Contents of `curriculum/{docPath}` @@ -58,11 +63,11 @@ TODO Fields: -- key: (string, a mobx id) +- key: (string, the id of the document in firebase) - title: (string) - type: (string, eg "problem") -- uid: (string, id of user who owns this document) -- contextId: (always "Ignored"?) +- 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) @@ -73,6 +78,7 @@ Fields: Collection: - comments +- history #### Contents of `documents/{docId}/comments/{commentId}` From aa021f62be89411a2353397f2c0fc343d4a0c2d2 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 14 Aug 2024 15:37:54 -0400 Subject: [PATCH 047/127] doc fix --- docs/firestore-schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/firestore-schema.md b/docs/firestore-schema.md index bd55a2d6be..2343a901ec 100644 --- a/docs/firestore-schema.md +++ b/docs/firestore-schema.md @@ -71,7 +71,7 @@ Fields: - context_id: (string, uuid, should match context_id of a class) - createdAt: (timestamp) - network: (string, name of a network) -- originDoc: ? +- 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_ From 838a10adf3519ba132530881033dee24b1325529 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 14 Aug 2024 16:51:52 -0400 Subject: [PATCH 048/127] Don't allow rewriting context_id in document metadata --- firebase-test/src/documents-rules.test.ts | 38 ++++++++++++++++++++++- firestore.rules | 2 +- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index 1bff2a95ee..74aa61097b 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -192,12 +192,48 @@ describe("Firestore security rules", () => { await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title" }); }); - it("authenticated teachers can't update user documents' read-only fields", async () => { + 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 network field", async () => { + db = initFirestore(teacherAuth); + await specClassDoc(thisClass, teacherId); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); + await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", network: "new-network" }); + }); + + 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 adminWriteDoc(kDocumentDocPath, specDocumentDoc()); diff --git a/firestore.rules b/firestore.rules index 0b579c3c40..f7c5cef9cd 100644 --- a/firestore.rules +++ b/firestore.rules @@ -94,7 +94,7 @@ service cloud.firestore { } function preservesReadOnlyDocumentFields() { - let readOnlyFieldsSet = ["uid", "network", "type", "key", "createdAt"].toSet(); + let readOnlyFieldsSet = ["uid", "network", "type", "key", "createdAt", "context_id"].toSet(); let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys(); return !affectedFieldsSet.hasAny(readOnlyFieldsSet); } From 5449ca09b1ed38105b923eb712ba1f30f5f4c80c Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Wed, 14 Aug 2024 17:37:12 -0400 Subject: [PATCH 049/127] move old functions to a functions-v1 folder --- firebase.json | 2 +- {functions => functions-v1}/.eslintrc.js | 0 {functions => functions-v1}/.gitignore | 0 {functions => functions-v1}/dependencies-notes.md | 0 {functions => functions-v1}/jest.config.js | 0 {functions => functions-v1}/package-lock.json | 0 {functions => functions-v1}/package.json | 0 {functions => functions-v1}/src/canonicalize-url.ts | 0 {functions => functions-v1}/src/get-image-data.ts | 0 {functions => functions-v1}/src/get-network-document.ts | 0 {functions => functions-v1}/src/get-network-resources.ts | 0 {functions => functions-v1}/src/index.ts | 0 {functions => functions-v1}/src/parse-document-content.ts | 0 {functions => functions-v1}/src/portal-types.ts | 0 {functions => functions-v1}/src/post-document-comment.ts | 0 {functions => functions-v1}/src/publish-support.ts | 0 {functions => functions-v1}/src/shared-utils.test.ts | 0 {functions => functions-v1}/src/shared-utils.ts | 0 {functions => functions-v1}/src/shared.ts | 0 {functions => functions-v1}/src/user-context.ts | 0 .../src/validate-commentable-document.ts | 0 {functions => functions-v1}/test/canonicalize-url.test.ts | 0 {functions => functions-v1}/test/get-image-data.test.ts | 0 {functions => functions-v1}/test/get-network-document.test.ts | 0 {functions => functions-v1}/test/get-network-resources.test.ts | 0 {functions => functions-v1}/test/parse-document-content.test.ts | 0 {functions => functions-v1}/test/post-document-comment.test.ts | 0 {functions => functions-v1}/test/publish-support.test.ts | 0 {functions => functions-v1}/test/shared-dataset-example.ts | 0 {functions => functions-v1}/test/shared.test.ts | 0 {functions => functions-v1}/test/test-utils.ts | 0 {functions => functions-v1}/test/user-context.test.ts | 0 .../test/validate-commentable-document.test.ts | 0 {functions => functions-v1}/tsconfig.json | 0 {functions => functions-v1}/tsconfig.prod.json | 0 35 files changed, 1 insertion(+), 1 deletion(-) rename {functions => functions-v1}/.eslintrc.js (100%) rename {functions => functions-v1}/.gitignore (100%) rename {functions => functions-v1}/dependencies-notes.md (100%) rename {functions => functions-v1}/jest.config.js (100%) rename {functions => functions-v1}/package-lock.json (100%) rename {functions => functions-v1}/package.json (100%) rename {functions => functions-v1}/src/canonicalize-url.ts (100%) rename {functions => functions-v1}/src/get-image-data.ts (100%) rename {functions => functions-v1}/src/get-network-document.ts (100%) rename {functions => functions-v1}/src/get-network-resources.ts (100%) rename {functions => functions-v1}/src/index.ts (100%) rename {functions => functions-v1}/src/parse-document-content.ts (100%) rename {functions => functions-v1}/src/portal-types.ts (100%) rename {functions => functions-v1}/src/post-document-comment.ts (100%) rename {functions => functions-v1}/src/publish-support.ts (100%) rename {functions => functions-v1}/src/shared-utils.test.ts (100%) rename {functions => functions-v1}/src/shared-utils.ts (100%) rename {functions => functions-v1}/src/shared.ts (100%) rename {functions => functions-v1}/src/user-context.ts (100%) rename {functions => functions-v1}/src/validate-commentable-document.ts (100%) rename {functions => functions-v1}/test/canonicalize-url.test.ts (100%) rename {functions => functions-v1}/test/get-image-data.test.ts (100%) rename {functions => functions-v1}/test/get-network-document.test.ts (100%) rename {functions => functions-v1}/test/get-network-resources.test.ts (100%) rename {functions => functions-v1}/test/parse-document-content.test.ts (100%) rename {functions => functions-v1}/test/post-document-comment.test.ts (100%) rename {functions => functions-v1}/test/publish-support.test.ts (100%) rename {functions => functions-v1}/test/shared-dataset-example.ts (100%) rename {functions => functions-v1}/test/shared.test.ts (100%) rename {functions => functions-v1}/test/test-utils.ts (100%) rename {functions => functions-v1}/test/user-context.test.ts (100%) rename {functions => functions-v1}/test/validate-commentable-document.test.ts (100%) rename {functions => functions-v1}/tsconfig.json (100%) rename {functions => functions-v1}/tsconfig.prod.json (100%) diff --git a/firebase.json b/firebase.json index ea7268ba54..dbc3f6a7b3 100644 --- a/firebase.json +++ b/firebase.json @@ -25,7 +25,7 @@ "npm --prefix \"$RESOURCE_DIR\" run lint", "npm --prefix \"$RESOURCE_DIR\" run build" ], - "source": "functions", + "source": "functions-v1", "ignore": [ "*.log", ".*", 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/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 100% rename from functions/package.json rename to functions-v1/package.json diff --git a/functions/src/canonicalize-url.ts b/functions-v1/src/canonicalize-url.ts similarity index 100% rename from functions/src/canonicalize-url.ts rename to functions-v1/src/canonicalize-url.ts diff --git a/functions/src/get-image-data.ts b/functions-v1/src/get-image-data.ts similarity index 100% rename from functions/src/get-image-data.ts rename to functions-v1/src/get-image-data.ts diff --git a/functions/src/get-network-document.ts b/functions-v1/src/get-network-document.ts similarity index 100% rename from functions/src/get-network-document.ts rename to functions-v1/src/get-network-document.ts diff --git a/functions/src/get-network-resources.ts b/functions-v1/src/get-network-resources.ts similarity index 100% rename from functions/src/get-network-resources.ts rename to functions-v1/src/get-network-resources.ts 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 100% rename from functions/src/parse-document-content.ts rename to functions-v1/src/parse-document-content.ts 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 100% rename from functions/src/post-document-comment.ts rename to functions-v1/src/post-document-comment.ts diff --git a/functions/src/publish-support.ts b/functions-v1/src/publish-support.ts similarity index 100% rename from functions/src/publish-support.ts rename to functions-v1/src/publish-support.ts diff --git a/functions/src/shared-utils.test.ts b/functions-v1/src/shared-utils.test.ts similarity index 100% rename from functions/src/shared-utils.test.ts rename to functions-v1/src/shared-utils.test.ts diff --git a/functions/src/shared-utils.ts b/functions-v1/src/shared-utils.ts similarity index 100% rename from functions/src/shared-utils.ts rename to functions-v1/src/shared-utils.ts diff --git a/functions/src/shared.ts b/functions-v1/src/shared.ts similarity index 100% rename from functions/src/shared.ts rename to functions-v1/src/shared.ts diff --git a/functions/src/user-context.ts b/functions-v1/src/user-context.ts similarity index 100% rename from functions/src/user-context.ts rename to functions-v1/src/user-context.ts diff --git a/functions/src/validate-commentable-document.ts b/functions-v1/src/validate-commentable-document.ts similarity index 100% rename from functions/src/validate-commentable-document.ts rename to functions-v1/src/validate-commentable-document.ts diff --git a/functions/test/canonicalize-url.test.ts b/functions-v1/test/canonicalize-url.test.ts similarity index 100% rename from functions/test/canonicalize-url.test.ts rename to functions-v1/test/canonicalize-url.test.ts diff --git a/functions/test/get-image-data.test.ts b/functions-v1/test/get-image-data.test.ts similarity index 100% rename from functions/test/get-image-data.test.ts rename to functions-v1/test/get-image-data.test.ts diff --git a/functions/test/get-network-document.test.ts b/functions-v1/test/get-network-document.test.ts similarity index 100% rename from functions/test/get-network-document.test.ts rename to functions-v1/test/get-network-document.test.ts diff --git a/functions/test/get-network-resources.test.ts b/functions-v1/test/get-network-resources.test.ts similarity index 100% rename from functions/test/get-network-resources.test.ts rename to functions-v1/test/get-network-resources.test.ts diff --git a/functions/test/parse-document-content.test.ts b/functions-v1/test/parse-document-content.test.ts similarity index 100% rename from functions/test/parse-document-content.test.ts rename to functions-v1/test/parse-document-content.test.ts diff --git a/functions/test/post-document-comment.test.ts b/functions-v1/test/post-document-comment.test.ts similarity index 100% rename from functions/test/post-document-comment.test.ts rename to functions-v1/test/post-document-comment.test.ts diff --git a/functions/test/publish-support.test.ts b/functions-v1/test/publish-support.test.ts similarity index 100% rename from functions/test/publish-support.test.ts rename to functions-v1/test/publish-support.test.ts 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/shared.test.ts b/functions-v1/test/shared.test.ts similarity index 100% rename from functions/test/shared.test.ts rename to functions-v1/test/shared.test.ts diff --git a/functions/test/test-utils.ts b/functions-v1/test/test-utils.ts similarity index 100% rename from functions/test/test-utils.ts rename to functions-v1/test/test-utils.ts diff --git a/functions/test/user-context.test.ts b/functions-v1/test/user-context.test.ts similarity index 100% rename from functions/test/user-context.test.ts rename to functions-v1/test/user-context.test.ts diff --git a/functions/test/validate-commentable-document.test.ts b/functions-v1/test/validate-commentable-document.test.ts similarity index 100% rename from functions/test/validate-commentable-document.test.ts rename to functions-v1/test/validate-commentable-document.test.ts diff --git a/functions/tsconfig.json b/functions-v1/tsconfig.json similarity index 100% rename from functions/tsconfig.json rename to functions-v1/tsconfig.json 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 From a05e7131932b7febe97b8d8443675a8e8cf92121 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 14 Aug 2024 17:54:44 -0400 Subject: [PATCH 050/127] Allow student access to metadata docs. --- firebase-test/src/documents-rules.test.ts | 58 ++++++++++++++++++----- firestore.rules | 11 +++-- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index 74aa61097b..a4a1972c3a 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -102,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); @@ -123,21 +116,25 @@ 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 specClassDoc(thisClass, teacherId); 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"] })); }); @@ -175,7 +172,7 @@ describe("Firestore security rules", () => { db = initFirestore(teacherAuth); await specClassDoc(thisClass, teacherId); await specClassDoc(otherClass, teacherId); - await adminWriteDoc(kDocumentDocPath, ({ add: { context_id: otherClass }})); + await adminWriteDoc(kDocumentDocPath, specDocumentDoc({ add: { context_id: otherClass }})); await expectUpdateToSucceed(db, kDocumentDocPath, { title: "new-title" }); }); @@ -236,18 +233,21 @@ describe("Firestore security rules", () => { 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); }); @@ -272,15 +272,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); }); }); diff --git a/firestore.rules b/firestore.rules index f7c5cef9cd..14f347411b 100644 --- a/firestore.rules +++ b/firestore.rules @@ -139,7 +139,8 @@ service cloud.firestore { } function isValidDocumentUpdateRequest() { - return userInResourceTeachers() && preservesReadOnlyDocumentFields(); + return preservesReadOnlyDocumentFields() && + ( resourceInUserClass() || userInResourceTeachers() ); } // return list of networks available to the current teacher @@ -303,14 +304,14 @@ 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 getDocumentData() { return get(/databases/$(database)/documents/authed/$(portal)/documents/$(docId)).data; From ba165861f4b794a390963ffd07cf1fc1a8d957a9 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 13 Aug 2024 15:10:09 -0400 Subject: [PATCH 051/127] feat: add unit, inv., problem when creating metadata docs (PT-188066940) [#188066940](https://www.pivotaltracker.com/story/show/188066940) --- functions/src/shared.ts | 6 ++++ src/lib/db-types.ts | 3 ++ src/lib/db.ts | 54 +++++++++++++++++++++++++++--- src/models/document/document.ts | 3 +- src/models/history/tree-manager.ts | 9 +---- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/functions/src/shared.ts b/functions/src/shared.ts index 5563c8c7e3..30d899a642 100644 --- a/functions/src/shared.ts +++ b/functions/src/shared.ts @@ -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; diff --git a/src/lib/db-types.ts b/src/lib/db-types.ts index c8257217fe..844f60b87e 100644 --- a/src/lib/db-types.ts +++ b/src/lib/db-types.ts @@ -60,7 +60,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.ts b/src/lib/db.ts index 131306d0a6..304cf27a02 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 "../../functions/src/shared"; import { getFirebaseFunction } from "../hooks/use-firebase-function"; import { IStores } from "../models/stores/stores"; import { TeacherSupportModelType, SectionTarget, AudienceModelType } from "../models/stores/supports"; @@ -396,9 +397,48 @@ export class DB { }); } - public createDocument(params: { type: DBDocumentType, content?: string }) { + 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, + contextId: "ignored", + key: documentKey, + properties: {}, + uid: userContext.uid, + }; + if ("offeringId" in metadata) { + 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 }) { const { type, content } = params; - const {user} = this.stores; + 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!; @@ -430,7 +470,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}); }) diff --git a/src/models/document/document.ts b/src/models/document/document.ts index 9cec2fe16c..ba1d8b73cb 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -111,7 +111,8 @@ export const DocumentModel = Tree.named("Document") // 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 } as IDocumentMetadata; }, getProperty(key: string) { return self.properties.get(key); diff --git a/src/models/history/tree-manager.ts b/src/models/history/tree-manager.ts index 38286d21b4..089f175383 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 "../../../functions/src/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; -} From e83b67a5d6803e969ddb7414d78b8e55526c23a6 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Thu, 15 Aug 2024 14:36:32 -0400 Subject: [PATCH 052/127] fix: correctly set visibility in `fetchFullDocument` --- src/components/document/sorted-section.tsx | 2 +- src/models/stores/sorted-documents.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index 9670e359ac..7318a89131 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -34,7 +34,7 @@ export const SortedSection: React.FC = observer(function SortedDocuments if (document) return document; // Calling `fetchFullDocument` will update the `documents` store with the full document, - // triggering a re-render of this component since its an observer. + // triggering a re-render of this component since it's an observer. sortedDocuments.fetchFullDocument(docKey); return undefined; diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 6f387a708a..b52b9ccd43 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -234,6 +234,9 @@ export class SortedDocuments { 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, @@ -241,7 +244,7 @@ export class SortedDocuments { properties: metadataDoc?.properties, userId: metadataDoc?.uid, groupId: undefined, - visibility: undefined, + visibility, originDoc: undefined, pubVersion: undefined, problem: metadataDoc?.problem, From 8eef853764b027899fd7dee09f32632d89be596d Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 16 Aug 2024 07:46:38 -0400 Subject: [PATCH 053/127] Refactor commented-docs.tsx with a separate model class --- src/components/chat/commented-documents.tsx | 126 +++--------------- src/models/commented-documents.ts | 135 ++++++++++++++++++++ 2 files changed, 152 insertions(+), 109 deletions(-) create mode 100644 src/models/commented-documents.ts diff --git a/src/components/chat/commented-documents.tsx b/src/components/chat/commented-documents.tsx index b7840e544a..dae52775e2 100644 --- a/src/components/chat/commented-documents.tsx +++ b/src/components/chat/commented-documents.tsx @@ -1,13 +1,12 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "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, CurriculumDocumentInfo, StudentDocumentInfo } from "../../models/commented-documents"; import DocumentIcon from "../../assets/icons/document-icon.svg"; @@ -17,16 +16,6 @@ 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}) => { const [db] = useFirestore(); @@ -36,109 +25,30 @@ 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]); - - //------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]); + const [commentedDocumentsQuery, setCommentedDocumentsQuery] = useState(); + const [curricumDocs, setCurricumDocs] = useState([]); + const [studentDocs, setStudentDocs] = useState([]); - //------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); + useEffect(() => { + if (user) { + setCommentedDocumentsQuery(new CommentedDocumentsQuery(db, user, unit, problem)); } - }, [mDocsRef, user?.network, user?.id]); + }, [user, db, unit, problem]); - //------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); - }); - + commentedDocumentsQuery?.queryCurriculumDocs().then(() => { + setCurricumDocs(commentedDocumentsQuery.getCurricumDocs()); + }); + commentedDocumentsQuery?.queryStudentDocs().then(() => { + setStudentDocs(commentedDocumentsQuery.getStudentDocs()); }); - return () => unsubscribeFromDocs?.(); - },[mDocsRef, mDocsInScopeRef]); + }, [commentedDocumentsQuery]); return (
{ - docsCommentedOn && - (docsCommentedOn).map((doc: PromisedCurriculumDocument, index:number) => { //Problem + Teacher Guide documents + (curricumDocs).map((doc, index) => { const {navTab} = getTabsOfCurriculumDoc(doc.path); return (
= ({user, handleDocView}) => { }) } { - workDocuments && - (workDocuments).map((doc: PromisedDocumentDocument, index: number) =>{ - //"Student Workspaces/"My Work"/"Class Work" + (studentDocs).map((doc, index) =>{ const sectionDoc = store.documents.getDocument(doc.key); const networkDoc = store.networkDocuments.getDocument(doc.key); if (sectionDoc){ diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts new file mode 100644 index 0000000000..fc433d847a --- /dev/null +++ b/src/models/commented-documents.ts @@ -0,0 +1,135 @@ +import { makeAutoObservable } from "mobx"; +import { Firestore } from "../lib/firestore"; +import { 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 StudentDocumentInfo { + id: string; + key: string; + title: string; + numComments: number; +} + +export class CommentedDocumentsQuery { + db: Firestore; + user: UserModelType; + unit: string; + problem: string; + + curriculumDocs: CurriculumDocumentInfo[] = []; + studentDocs: StudentDocumentInfo[] = []; + + constructor( + db: Firestore, + user: UserModelType, + unit: string, + problem: string) { + makeAutoObservable(this); + this.db = db; + this.user = user; + this.unit = unit; + this.problem = problem; + } + + getCurricumDocs(): CurriculumDocumentInfo[] { + return this.curriculumDocs; + } + + getStudentDocs(): StudentDocumentInfo[] { + return this.studentDocs; + } + + 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); + this.curriculumDocs = commentedDocs; + } + +// classes = db.collection("classes").where("teachers", "array-contains", user.id) OR db.collection("classes").where("network", "==", user.network); +// where "context_id" is in classes. Iterate over classes because there's a limit of ~10 for "in" queries. + + + async queryStudentDocs() { + console.log("running queryStudentDocs"); + const collection = this.db.collection("documents"); + let docsQuery; + if(this.user.network){ + docsQuery = collection.where("network", "==", this.user.network); + } else { + docsQuery = collection.where("uid", "==", this.user.id); + } + const result = await docsQuery.get(); + console.log('query result:', result.docs.length); + const docs: StudentDocumentInfo[] = result.docs.map(doc => { + const data = doc.data() as DocumentDocument; + return { + id: doc.id, + numComments: 0, + title: "temp", + ...data + }; + }); + const commentedDocs: StudentDocumentInfo[] = []; + const promiseArr: Promise[] = []; + // TODO maybe combine multiple "docs" that have same ID? + for (let doc of docs){ + const docCommentsRef = collection.doc(doc.id).collection("comments"); + promiseArr.push(docCommentsRef.get().then((qs)=>{ + console.log('comments:', docCommentsRef.path, qs.size); + if (qs.empty === false){ + doc = {...doc, numComments: qs.size}; + commentedDocs.push(doc); + } + })); + } + await Promise.all(promiseArr); + this.studentDocs = commentedDocs; + console.log('stored studentDocs:', this.studentDocs); + } + +} From fe581270226401b6e90b2ff519f89309d81f6644 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 16 Aug 2024 09:37:41 -0400 Subject: [PATCH 054/127] Update query to check teacher classes --- src/models/commented-documents.ts | 63 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts index fc433d847a..880f666d81 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -1,6 +1,7 @@ import { makeAutoObservable } from "mobx"; +import { chunk } from "lodash"; import { Firestore } from "../lib/firestore"; -import { CurriculumDocument, DocumentDocument } from "../lib/firestore-schema"; +import { ClassDocument, CurriculumDocument, DocumentDocument } from "../lib/firestore-schema"; import { getSectionTitle } from "./curriculum/section"; import { UserModelType } from "./stores/user"; @@ -90,46 +91,56 @@ export class CommentedDocumentsQuery { this.curriculumDocs = commentedDocs; } -// classes = db.collection("classes").where("teachers", "array-contains", user.id) OR db.collection("classes").where("network", "==", user.network); -// where "context_id" is in classes. Iterate over classes because there's a limit of ~10 for "in" queries. - - async queryStudentDocs() { console.log("running queryStudentDocs"); + + // 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("network", "==", this.user.network).get()).docs + : []; + const allClasses = individualClasses.concat(networkClasses); + console.log("teacher classes:", individualClasses, networkClasses); + const classIds = allClasses.map(doc => { return (doc.data() as ClassDocument).context_id; }); + + // Find student documents + if (classIds.length === 0) { + return; + } const collection = this.db.collection("documents"); - let docsQuery; - if(this.user.network){ - docsQuery = collection.where("network", "==", this.user.network); - } else { - docsQuery = collection.where("uid", "==", this.user.id); + // 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: StudentDocumentInfo[] = []; + 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 result = await docsQuery.get(); - console.log('query result:', result.docs.length); - const docs: StudentDocumentInfo[] = result.docs.map(doc => { - const data = doc.data() as DocumentDocument; - return { - id: doc.id, - numComments: 0, - title: "temp", - ...data - }; - }); const commentedDocs: StudentDocumentInfo[] = []; const promiseArr: Promise[] = []; // TODO maybe combine multiple "docs" that have same ID? - for (let doc of docs){ + 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)=>{ - console.log('comments:', docCommentsRef.path, qs.size); if (qs.empty === false){ - doc = {...doc, numComments: qs.size}; - commentedDocs.push(doc); + const commentedDoc = {...doc, numComments: qs.size}; + commentedDocs.push(commentedDoc); } })); } await Promise.all(promiseArr); this.studentDocs = commentedDocs; - console.log('stored studentDocs:', this.studentDocs); } } From d772e75b42d69ac882d23af83dac0fb099a685e2 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Sat, 17 Aug 2024 17:45:53 -0400 Subject: [PATCH 055/127] move code shared between functions and runtime --- functions-v1/src/canonicalize-url.ts | 2 +- functions-v1/src/get-image-data.ts | 2 +- functions-v1/src/get-network-document.ts | 2 +- functions-v1/src/get-network-resources.ts | 2 +- functions-v1/src/parse-document-content.ts | 4 ++-- functions-v1/src/post-document-comment.ts | 2 +- functions-v1/src/publish-support.ts | 4 ++-- functions-v1/src/user-context.ts | 2 +- functions-v1/src/validate-commentable-document.ts | 2 +- functions-v1/test/canonicalize-url.test.ts | 2 +- functions-v1/test/get-image-data.test.ts | 2 +- functions-v1/test/get-network-document.test.ts | 4 ++-- functions-v1/test/get-network-resources.test.ts | 2 +- functions-v1/test/parse-document-content.test.ts | 2 +- functions-v1/test/post-document-comment.test.ts | 2 +- functions-v1/test/publish-support.test.ts | 4 ++-- functions-v1/test/test-utils.ts | 2 +- functions-v1/test/user-context.test.ts | 2 +- .../test/validate-commentable-document.test.ts | 2 +- functions-v1/tsconfig.json | 4 ++-- {functions-v1/src => shared}/shared-utils.test.ts | 6 +++--- {functions-v1/src => shared}/shared-utils.ts | 0 {functions-v1/test => shared}/shared.test.ts | 5 +++-- {functions-v1/src => shared}/shared.ts | 12 ++++++------ src/components/document/doc-list-debug.tsx | 2 +- src/components/document/document-group.tsx | 2 +- src/components/document/sorted-section.tsx | 2 +- .../navigation/network-documents-section.tsx | 2 +- .../thumbnail/collapsible-document-section.tsx | 2 +- src/components/thumbnail/simple-document-item.tsx | 2 +- src/hooks/document-comment-hooks.ts | 2 +- src/hooks/network-resources.test.ts | 2 +- src/hooks/network-resources.ts | 2 +- src/hooks/use-document-sync-to-firebase.ts | 2 +- src/hooks/use-stores.ts | 2 +- src/hooks/use-user-context.ts | 2 +- src/lib/db.ts | 2 +- src/models/curriculum/log-curriculum-event.ts | 2 +- src/models/curriculum/section.ts | 2 +- src/models/curriculum/unit.ts | 2 +- src/models/document/document.ts | 2 +- src/models/document/log-document-event.ts | 2 +- src/models/history/log-history-event.ts | 2 +- src/models/history/tree-manager.ts | 2 +- src/models/image-map.test.ts | 2 +- src/models/stores/document-group.test.ts | 2 +- src/models/stores/document-group.ts | 2 +- src/models/stores/persistent-ui.ts | 2 +- src/models/stores/sorted-documents.test.ts | 2 +- src/models/stores/sorted-documents.ts | 2 +- src/models/tiles/log/log-comment-event.ts | 2 +- src/utilities/sort-document-utils.ts | 2 +- tsconfig.json | 2 +- 53 files changed, 66 insertions(+), 65 deletions(-) rename {functions-v1/src => shared}/shared-utils.test.ts (97%) rename {functions-v1/src => shared}/shared-utils.ts (100%) rename {functions-v1/test => shared}/shared.test.ts (96%) rename {functions-v1/src => shared}/shared.ts (98%) diff --git a/functions-v1/src/canonicalize-url.ts b/functions-v1/src/canonicalize-url.ts index 61d1cc28e8..931d999320 100644 --- a/functions-v1/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-v1/src/get-image-data.ts b/functions-v1/src/get-image-data.ts index 4db8cc3647..9162ef1ae1 100644 --- a/functions-v1/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-v1/src/get-network-document.ts b/functions-v1/src/get-network-document.ts index f42e0f7e14..129a0cbb0e 100644 --- a/functions-v1/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-v1/src/get-network-resources.ts b/functions-v1/src/get-network-resources.ts index b6b672e554..7e09e91513 100644 --- a/functions-v1/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-v1/src/parse-document-content.ts b/functions-v1/src/parse-document-content.ts index aafecb72fa..872227ac7d 100644 --- a/functions-v1/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-v1/src/post-document-comment.ts b/functions-v1/src/post-document-comment.ts index 062b443ea7..d0e4bc2b07 100644 --- a/functions-v1/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-v1/src/publish-support.ts b/functions-v1/src/publish-support.ts index ca4ae5bd93..0215a35ebf 100644 --- a/functions-v1/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-v1/src/user-context.ts b/functions-v1/src/user-context.ts index 87183f1f9d..02bc8c64b4 100644 --- a/functions-v1/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-v1/src/validate-commentable-document.ts b/functions-v1/src/validate-commentable-document.ts index 4c37811061..dcfe48f735 100644 --- a/functions-v1/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-v1/test/canonicalize-url.test.ts b/functions-v1/test/canonicalize-url.test.ts index 427a9a6e9f..95f94b9e87 100644 --- a/functions-v1/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-v1/test/get-image-data.test.ts b/functions-v1/test/get-image-data.test.ts index 496d1c7b13..219f674b1b 100644 --- a/functions-v1/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-v1/test/get-network-document.test.ts b/functions-v1/test/get-network-document.test.ts index 064b2cd2af..14bacb2e04 100644 --- a/functions-v1/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-v1/test/get-network-resources.test.ts b/functions-v1/test/get-network-resources.test.ts index 82c6e2e215..474e8daedf 100644 --- a/functions-v1/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-v1/test/parse-document-content.test.ts b/functions-v1/test/parse-document-content.test.ts index 7c2abad953..5a1e6b2db0 100644 --- a/functions-v1/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-v1/test/post-document-comment.test.ts b/functions-v1/test/post-document-comment.test.ts index c1bdd4c008..4de8cc25ed 100644 --- a/functions-v1/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-v1/test/publish-support.test.ts b/functions-v1/test/publish-support.test.ts index 21c5e2cd3a..83fd9d79fc 100644 --- a/functions-v1/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-v1/test/test-utils.ts b/functions-v1/test/test-utils.ts index d5d8c4c84f..d18af8ae7f 100644 --- a/functions-v1/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-v1/test/user-context.test.ts b/functions-v1/test/user-context.test.ts index 56d881e0f8..44998b6d3f 100644 --- a/functions-v1/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-v1/test/validate-commentable-document.test.ts b/functions-v1/test/validate-commentable-document.test.ts index 0af37b9c08..22b5fa858a 100644 --- a/functions-v1/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-v1/tsconfig.json b/functions-v1/tsconfig.json index 43e7e79b5a..08b303e1f1 100644 --- a/functions-v1/tsconfig.json +++ b/functions-v1/tsconfig.json @@ -14,6 +14,6 @@ "compileOnSave": true, "include": [ "src", - "test" - ] + "test", + "../shared" ] } diff --git a/functions-v1/src/shared-utils.test.ts b/shared/shared-utils.test.ts similarity index 97% rename from functions-v1/src/shared-utils.test.ts rename to shared/shared-utils.test.ts index acd4f95b74..f840efd814 100644 --- a/functions-v1/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-v1/src/shared-utils.ts b/shared/shared-utils.ts similarity index 100% rename from functions-v1/src/shared-utils.ts rename to shared/shared-utils.ts diff --git a/functions-v1/test/shared.test.ts b/shared/shared.test.ts similarity index 96% rename from functions-v1/test/shared.test.ts rename to shared/shared.test.ts index 79b69d5abf..0a758162b6 100644 --- a/functions-v1/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-v1/src/shared.ts b/shared/shared.ts similarity index 98% rename from functions-v1/src/shared.ts rename to shared/shared.ts index 5563c8c7e3..6b4697390c 100644 --- a/functions-v1/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. diff --git a/src/components/document/doc-list-debug.tsx b/src/components/document/doc-list-debug.tsx index 3eda15fbaa..1bdd1c068d 100644 --- a/src/components/document/doc-list-debug.tsx +++ b/src/components/document/doc-list-debug.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { IDocumentMetadata } from "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import "./doc-list-debug.scss"; diff --git a/src/components/document/document-group.tsx b/src/components/document/document-group.tsx index e4ffcc2e77..ef65809054 100644 --- a/src/components/document/document-group.tsx +++ b/src/components/document/document-group.tsx @@ -3,7 +3,7 @@ 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 "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { DocumentGroup } from "../../models/stores/document-group"; import ScrollArrowIcon from "../../assets/workspace-instance-scroll.svg"; diff --git a/src/components/document/sorted-section.tsx b/src/components/document/sorted-section.tsx index 9670e359ac..b06bea36db 100644 --- a/src/components/document/sorted-section.tsx +++ b/src/components/document/sorted-section.tsx @@ -5,7 +5,7 @@ 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 "../../../functions/src/shared"; +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"; 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/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/simple-document-item.tsx b/src/components/thumbnail/simple-document-item.tsx index d1db754074..0808b869a0 100644 --- a/src/components/thumbnail/simple-document-item.tsx +++ b/src/components/thumbnail/simple-document-item.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { IDocumentMetadata } from "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { useStores } from "../../hooks/use-stores"; import "./simple-document-item.scss"; diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index e1d495a8d5..d998136a47 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"; 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.ts b/src/hooks/use-document-sync-to-firebase.ts index 6ecce9b816..419199971d 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -12,7 +12,7 @@ import { isPublishedType, LearningLogDocument, LearningLogPublication, PersonalD import { UserModelType } from "../models/stores/user"; import { Firestore } from "src/lib/firestore"; import { useMutation, UseMutationOptions } from "react-query"; -import { ITileMapEntry } from "functions/src/shared"; +import { ITileMapEntry } from "../../shared/shared"; import { DocumentContentSnapshotType } from "src/models/document/document-content"; import { IArrowAnnotation } from "src/models/annotations/arrow-annotation"; 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-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/lib/db.ts b/src/lib/db.ts index 131306d0a6..8ca6603bfd 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -30,7 +30,7 @@ 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 { 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"; 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/document.ts b/src/models/document/document.ts index 9cec2fe16c..16ae6e74b7 100644 --- a/src/models/document/document.ts +++ b/src/models/document/document.ts @@ -13,7 +13,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"; diff --git a/src/models/document/log-document-event.ts b/src/models/document/log-document-event.ts index 6c85360ada..c0a1404e55 100644 --- a/src/models/document/log-document-event.ts +++ b/src/models/document/log-document-event.ts @@ -1,4 +1,4 @@ -import { IDocumentMetadata } from "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { Logger } from "../../lib/logger"; import { LogEventMethod, LogEventName } from "../../lib/logger-types"; import { UserModelType } from "../stores/user"; 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..fc8268aa15 100644 --- a/src/models/history/tree-manager.ts +++ b/src/models/history/tree-manager.ts @@ -10,7 +10,7 @@ import { TreePatchRecord, HistoryEntry, TreePatchRecordSnapshot, import { DEBUG_HISTORY } from "../../lib/debug"; import { getFirebaseFunction } from "../../hooks/use-firebase-function"; import { ICommentableDocumentParams, IDocumentMetadata, - networkDocumentKey } from "../../../functions/src/shared"; + networkDocumentKey } from "../../../shared/shared"; import { Firestore } from "../../lib/firestore"; import { UserModelType } from "../stores/user"; import { UserContextProvider } from "../stores/user-context-provider"; 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/document-group.test.ts b/src/models/stores/document-group.test.ts index c98b4bc18c..598c33899c 100644 --- a/src/models/stores/document-group.test.ts +++ b/src/models/stores/document-group.test.ts @@ -5,7 +5,7 @@ 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 "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { ISortedDocumentsStores, SortedDocuments } from "./sorted-documents"; import { DB } from "../../lib/db"; import { mock } from "ts-jest-mocker"; diff --git a/src/models/stores/document-group.ts b/src/models/stores/document-group.ts index 5b680f1d20..24db168fdd 100644 --- a/src/models/stores/document-group.ts +++ b/src/models/stores/document-group.ts @@ -1,5 +1,5 @@ import { FC, SVGProps } from "react"; -import { IDocumentMetadata } from "functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { ISortedDocumentsStores, TagWithDocs } from "./sorted-documents"; import { makeAutoObservable } from "mobx"; import { diff --git a/src/models/stores/persistent-ui.ts b/src/models/stores/persistent-ui.ts index 2d186b69f0..1969e4c4fd 100644 --- a/src/models/stores/persistent-ui.ts +++ b/src/models/stores/persistent-ui.ts @@ -6,7 +6,7 @@ import { DocFilterType, DocFilterTypeEnum, kDividerHalf, kDividerMax, kDividerMi 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"; diff --git a/src/models/stores/sorted-documents.test.ts b/src/models/stores/sorted-documents.test.ts index 422b363128..43c1b03844 100644 --- a/src/models/stores/sorted-documents.test.ts +++ b/src/models/stores/sorted-documents.test.ts @@ -6,7 +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 "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import "../tiles/text/text-registration"; import "../../plugins/drawing/drawing-registration"; diff --git a/src/models/stores/sorted-documents.ts b/src/models/stores/sorted-documents.ts index 6f387a708a..71ffc84fef 100644 --- a/src/models/stores/sorted-documents.ts +++ b/src/models/stores/sorted-documents.ts @@ -7,7 +7,7 @@ import { DB } from "../../lib/db"; import { AppConfigModelType } from "./app-config-model"; import { Bookmarks } from "./bookmarks"; import { UserModelType } from "./user"; -import { IDocumentMetadata } from "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { typeConverter } from "../../utilities/db-utils"; import { createDocMapByBookmarks, 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/utilities/sort-document-utils.ts b/src/utilities/sort-document-utils.ts index d92f1d76b1..60175323b0 100644 --- a/src/utilities/sort-document-utils.ts +++ b/src/utilities/sort-document-utils.ts @@ -1,4 +1,4 @@ -import { IDocumentMetadata } from "functions/src/shared"; +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"; 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", "**/.*/"] } From 46937cd0a8b279cb6bad67d168ef9654d74f7e86 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Sat, 17 Aug 2024 17:57:21 -0400 Subject: [PATCH 056/127] basic firebase function it monitors firestore for a specific write and logs it --- firebase.json | 50 +- functions-v2/.eslintrc.js | 35 + functions-v2/.gitignore | 10 + functions-v2/jest.config.js | 4 + functions-v2/package-lock.json | 12834 +++++++++++++++++++++++++++++++ functions-v2/package.json | 39 + functions-v2/src/index.ts | 7 + functions-v2/tsconfig.dev.json | 5 + functions-v2/tsconfig.json | 21 + 9 files changed, 12990 insertions(+), 15 deletions(-) create mode 100644 functions-v2/.eslintrc.js create mode 100644 functions-v2/.gitignore create mode 100644 functions-v2/jest.config.js create mode 100644 functions-v2/package-lock.json create mode 100644 functions-v2/package.json create mode 100644 functions-v2/src/index.ts create mode 100644 functions-v2/tsconfig.dev.json create mode 100644 functions-v2/tsconfig.json diff --git a/firebase.json b/firebase.json index dbc3f6a7b3..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-v1", - "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/functions-v2/.eslintrc.js b/functions-v2/.eslintrc.js new file mode 100644 index 0000000000..d508d8596f --- /dev/null +++ b/functions-v2/.eslintrc.js @@ -0,0 +1,35 @@ +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], + }, +}; 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/jest.config.js b/functions-v2/jest.config.js new file mode 100644 index 0000000000..4a5b465ecb --- /dev/null +++ b/functions-v2/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/functions-v2/package-lock.json b/functions-v2/package-lock.json new file mode 100644 index 0000000000..3afbb3fb08 --- /dev/null +++ b/functions-v2/package-lock.json @@ -0,0 +1,12834 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "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.0.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.0.1.tgz", + "integrity": "sha512-1m+crtgAR8Tl36gjpM02KCY5zduAejFmDSXvih/DB93apg39f0U/WwRgT7sitGIRqyCcIpktNUbXJv7Y9JOF4A==", + "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..e30004443d --- /dev/null +++ b/functions-v2/package.json @@ -0,0 +1,39 @@ +{ + "name": "functions", + "scripts": { + "lint": "eslint --ext .js,.ts .", + "build": "tsc", + "build:watch": "tsc --watch", + "emulator": "firebase emulators:start --project demo-test", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "test": "jest", + "test:emulator": "firebase emulators:start --project demo-test --only firestore,database", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "20" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "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/index.ts b/functions-v2/src/index.ts new file mode 100644 index 0000000000..a657da9f78 --- /dev/null +++ b/functions-v2/src/index.ts @@ -0,0 +1,7 @@ +import {onDocumentWritten} from "firebase-functions/v2/firestore"; +import * as logger from "firebase-functions/logger"; + +export const updateClassDocNetworksOnUserChange = + onDocumentWritten("{root}/{space}/users/{userId}", (event) => { + logger.info("User updated", event.document); + }); 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..4990f4585e --- /dev/null +++ b/functions-v2/tsconfig.json @@ -0,0 +1,21 @@ +{ + "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"], + }, + "compileOnSave": true, + "include": [ + "src", + "test" + ], + "exclude": ["**/node_modules", "**/.*/"] +} From c3eac970a412e04da8bb42516811ea8d20efa150 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 19 Aug 2024 11:13:56 -0400 Subject: [PATCH 057/127] Improve rules, make tests pass, re-do class update logic --- firebase-test/src/documents-rules.test.ts | 23 +------- firestore.rules | 18 +++--- src/lib/firestore.ts | 4 ++ src/lib/teacher-network.test.ts | 24 +++++--- src/lib/teacher-network.ts | 71 +++++++++++++++++------ 5 files changed, 85 insertions(+), 55 deletions(-) diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index a4a1972c3a..f5a1f27d32 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -120,12 +120,6 @@ describe("Firestore security rules", () => { await expectWriteToFail(db, kDocumentDocPath, specDocumentDoc({ remove: ["uid"] })); }); - it("authenticated teachers can't write user documents without required network", async () => { - db = initFirestore(teacherAuth); - await specClassDoc(thisClass, teacherId); - 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); @@ -161,14 +155,14 @@ describe("Firestore security rules", () => { // 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.skip("authenticated teachers can write user documents in secondary class", async () => { + 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.skip("authenticated teachers can update user documents in secondary class", async () => { + it("authenticated teachers can update user documents in secondary class", async () => { db = initFirestore(teacherAuth); await specClassDoc(thisClass, teacherId); await specClassDoc(otherClass, teacherId); @@ -196,13 +190,6 @@ describe("Firestore security rules", () => { await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", uid: teacher2Id }); }); - it("authenticated teachers can't update user documents' read-only network field", async () => { - db = initFirestore(teacherAuth); - await specClassDoc(thisClass, teacherId); - await adminWriteDoc(kDocumentDocPath, specDocumentDoc()); - await expectUpdateToFail(db, kDocumentDocPath, { title: "new-title", network: "new-network" }); - }); - it("authenticated teachers can't update user documents' read-only type field", async () => { db = initFirestore(teacherAuth); await specClassDoc(thisClass, teacherId); @@ -592,12 +579,6 @@ describe("Firestore security rules", () => { await expectUpdateToFail(db, kDocumentCommentDocPath, { content: "A new comment!", uid: teacher2Id }); }); - it("authenticated teachers can't update document comments' read-only network field", async () => { - await initFirestoreWithUserDocument(teacherAuth); - await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); - await expectUpdateToFail(db, kDocumentCommentDocPath, { content: "A new comment!", network: "other-network" }); - }); - it("authenticated teachers can update document comments", async () => { await initFirestoreWithUserDocument(teacherAuth); await adminWriteDoc(kDocumentCommentDocPath, specCommentDoc()); diff --git a/firestore.rules b/firestore.rules index 14f347411b..152ccef4ad 100644 --- a/firestore.rules +++ b/firestore.rules @@ -87,14 +87,8 @@ service cloud.firestore { request.resource.data.keys().hasAll(["uid", "unit", "problem", "section", "path", "network"]); } - function isValidDocumentCreateRequest() { - return - classIsRequestContextId() && - request.resource.data.keys().hasAll(["uid", "network", "type", "key", "createdAt"]); - } - function preservesReadOnlyDocumentFields() { - let readOnlyFieldsSet = ["uid", "network", "type", "key", "createdAt", "context_id"].toSet(); + let readOnlyFieldsSet = ["uid", "type", "key", "createdAt", "context_id"].toSet(); let affectedFieldsSet = request.resource.data.diff(resource.data).affectedKeys(); return !affectedFieldsSet.hasAny(readOnlyFieldsSet); } @@ -126,8 +120,8 @@ service cloud.firestore { // 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)).data; - return class_doc != null && string(request.auth.token.platform_user_id) in class_doc.teachers; + 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 @@ -138,6 +132,12 @@ service cloud.firestore { 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() ); diff --git a/src/lib/firestore.ts b/src/lib/firestore.ts index eea8ba862e..f62c013b4f 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 diff --git a/src/lib/teacher-network.test.ts b/src/lib/teacher-network.test.ts index 4a403c86eb..5a8efef72a 100644 --- a/src/lib/teacher-network.test.ts +++ b/src/lib/teacher-network.test.ts @@ -34,7 +34,8 @@ const mockCollection = jest.fn((path: string) => { jest.mock("firebase/app", () => ({ firestore: () => ({ collection: mockCollection, - doc: mockDoc + doc: mockDoc, + runTransaction: jest.fn(callback => callback()) }) })); @@ -161,15 +162,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(); + 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; }); @@ -202,7 +209,7 @@ describe("Teacher network functions", () => { fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); + expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); return result; @@ -213,7 +220,7 @@ describe("Teacher network functions", () => { fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); const result = await syncClass(firestore, kPortalJWT, partClass1); - expect(mockDoc).toHaveBeenCalledWith(classDocPath); + expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).toHaveBeenCalledWith(fsClass1); return result; @@ -317,7 +324,8 @@ describe("Teacher network functions", () => { await syncTeacherClassesAndOfferings(firestore, user, kPortalJWT); expect(mockDoc).toHaveBeenCalledTimes(1); expect(mockDocGet).toHaveBeenCalledTimes(1); - expect(mockDocSet).toHaveBeenCalledTimes(0); + expect(mockDocSet).toHaveBeenCalledTimes(1); + expect(mockDocSet).toHaveBeenCalledWith({...fsClass1, network: undefined}); }); it("should sync classes and offerings when appropriate", async () => { diff --git a/src/lib/teacher-network.ts b/src/lib/teacher-network.ts index bdf84f9538..eb70431b14 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -1,7 +1,8 @@ +import { FieldValue } from "@google-cloud/firestore"; import { Optional } from "utility-types"; import { UserModelType } from "../models/stores/user"; import { arraysEqualIgnoringOrder } from "../utilities/js-utils"; -import { Firestore } from "./firestore"; +import { Firestore, isFirestorePermissionsError } from "./firestore"; import { ClassDocument, OfferingDocument } from "./firestore-schema"; import { IPortalClassInfo } from "./portal-types"; @@ -64,7 +65,7 @@ export function syncTeacherClassesAndOfferings(firestore: Firestore, user: UserM // synchronize the classes Object.keys(userClasses).forEach(async context_id => { - promises.push(syncClass(firestore, rawPortalJWT, userClasses[context_id])); + promises.push(syncClass(firestore, rawPortalJWT, userClasses[context_id], network)); }); if (network) { @@ -82,35 +83,71 @@ export function syncTeacherClassesAndOfferings(firestore: Firestore, user: UserM return Promise.all(promises); } -async function createOrUpdateClassDoc(firestore: Firestore, docPath: string, aClass: ClassDocument): - Promise { - return firestore.guaranteeDocument(docPath, - async () => { return aClass; }, - (content) => { return !content || !arraysEqualIgnoringOrder(aClass.teachers, content.teachers); } - ); +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)) { + console.log("updating teacher array:", data.teachers, aClass.teachers); + await docRef.update({ teachers: aClass.teachers }); + } + if (addNetwork && !data.network?.includes(addNetwork)) { + console.log("updating networks array:", data.network, addNetwork); + await docRef.update({ network: FieldValue.arrayUnion(addNetwork) }); + } + } else { + // Create the document. + console.log("new doc:", aClass, addNetwork); + if (addNetwork) { + await docRef.set({ ...aClass, network: addNetwork, networks: [addNetwork] }); + } else { + await docRef.set(aClass); + } + } + }); } -export async function syncClass(firestore: Firestore, rawPortalJWT: string, aClass: ClassWithoutTeachers) { - const { uri, context_id, network } = aClass; - const promises: Promise[] = []; +export async function syncClass(firestore: Firestore, rawPortalJWT: string, + aClass: ClassWithoutTeachers, addNetwork?: string) { + const { uri, context_id } = aClass; + const promises: Promise[] = []; if (uri && context_id && rawPortalJWT) { const teachers = await getClassTeachers(uri, rawPortalJWT); 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 (network) { - console.log('attempting to set new class doc:', `classes/${network}_${context_id}`, classWithTeachers); - promises.push(createOrUpdateClassDoc(firestore, `classes/${network}_${context_id}`, classWithTeachers)); + if (aClass.network) { + console.log('attempting to set old class doc:', `classes/${aClass.network}_${context_id}`, + classWithTeachers, addNetwork); + promises.push(createOrUpdateClassDoc(firestore, `classes/${aClass.network}_${context_id}`, + classWithTeachers, addNetwork)); } // New location of the class document - console.log('attempting to set new class doc:', `classes/${context_id}`, classWithTeachers); - promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers)); + console.log('attempting to set new class doc:', `classes/${context_id}`, classWithTeachers, addNetwork); + promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers, addNetwork)); } return Promise.all(promises); } From 466a8072c66b7a49cd0809117dfd4124d5b285ad Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 19 Aug 2024 12:20:59 -0400 Subject: [PATCH 058/127] chore: add null check, comment about contextId --- src/lib/db.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index 304cf27a02..45e6cb204f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -414,12 +414,14 @@ export class DB { 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, }; - if ("offeringId" in metadata) { + if ("offeringId" in metadata && metadata.offeringId != null) { const { investigation, problem, unit } = this.stores; const investigationOrdinal = String(investigation.ordinal); const problemOrdinal = String(problem.ordinal); From 77acf06cd130d854690542a58988d2148ae34877 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 19 Aug 2024 13:26:53 -0400 Subject: [PATCH 059/127] Fix firestore import --- src/lib/teacher-network.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/teacher-network.ts b/src/lib/teacher-network.ts index eb70431b14..1c256751e3 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -1,4 +1,4 @@ -import { FieldValue } from "@google-cloud/firestore"; +import firebase from "firebase/app"; import { Optional } from "utility-types"; import { UserModelType } from "../models/stores/user"; import { arraysEqualIgnoringOrder } from "../utilities/js-utils"; @@ -108,7 +108,7 @@ async function createOrUpdateClassDoc( } if (addNetwork && !data.network?.includes(addNetwork)) { console.log("updating networks array:", data.network, addNetwork); - await docRef.update({ network: FieldValue.arrayUnion(addNetwork) }); + await docRef.update({ network: firebase.firestore.FieldValue.arrayUnion(addNetwork) }); } } else { // Create the document. From 56cbf959880edb09c1b294a09c081a7c36782d09 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 19 Aug 2024 15:08:25 -0400 Subject: [PATCH 060/127] Commented docs cleanups --- src/components/chat/commented-documents.tsx | 25 ++++---------- src/models/commented-documents.ts | 38 +++++++++++++-------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/components/chat/commented-documents.tsx b/src/components/chat/commented-documents.tsx index dae52775e2..aafadf061b 100644 --- a/src/components/chat/commented-documents.tsx +++ b/src/components/chat/commented-documents.tsx @@ -6,7 +6,7 @@ import { useDocumentCaption } from "../../hooks/use-document-caption"; import { UserModelType } from "../../models/stores/user"; import { getNavTabOfDocument, getTabsOfCurriculumDoc, isStudentWorkspaceDoc } from "../../models/stores/persistent-ui"; import { DocumentModelType } from "../../models/document/document"; -import { CommentedDocumentsQuery, CurriculumDocumentInfo, StudentDocumentInfo } from "../../models/commented-documents"; +import { CommentedDocumentsQuery } from "../../models/commented-documents"; import DocumentIcon from "../../assets/icons/document-icon.svg"; @@ -25,30 +25,19 @@ export const CommentedDocuments: React.FC = ({user, handleDocView}) => { const problem = store.problemOrdinal; const unit = store.unit.code; - const [commentedDocumentsQuery, setCommentedDocumentsQuery] = useState(); - const [curricumDocs, setCurricumDocs] = useState([]); - const [studentDocs, setStudentDocs] = useState([]); - + const [commentedDocumentsQuery] + = useState(() => new CommentedDocumentsQuery(db, unit, problem)); useEffect(() => { if (user) { - setCommentedDocumentsQuery(new CommentedDocumentsQuery(db, user, unit, problem)); + commentedDocumentsQuery.setUser(user); } - }, [user, db, unit, problem]); - - useEffect(() => { - commentedDocumentsQuery?.queryCurriculumDocs().then(() => { - setCurricumDocs(commentedDocumentsQuery.getCurricumDocs()); - }); - commentedDocumentsQuery?.queryStudentDocs().then(() => { - setStudentDocs(commentedDocumentsQuery.getStudentDocs()); - }); - }, [commentedDocumentsQuery]); + }, [commentedDocumentsQuery, user]); return (
{ - (curricumDocs).map((doc, index) => { + (commentedDocumentsQuery.getCurricumDocs()).map((doc, index) => { const {navTab} = getTabsOfCurriculumDoc(doc.path); return (
= ({user, handleDocView}) => { }) } { - (studentDocs).map((doc, index) =>{ + (commentedDocumentsQuery.getUserDocs()).map((doc, index) =>{ const sectionDoc = store.documents.getDocument(doc.key); const networkDoc = store.networkDocuments.getDocument(doc.key); if (sectionDoc){ diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts index 880f666d81..f1283eb6fc 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable } from "mobx"; +import { makeAutoObservable, runInAction } from "mobx"; import { chunk } from "lodash"; import { Firestore } from "../lib/firestore"; import { ClassDocument, CurriculumDocument, DocumentDocument } from "../lib/firestore-schema"; @@ -14,7 +14,7 @@ export interface CurriculumDocumentInfo { numComments: number; } -export interface StudentDocumentInfo { +export interface UserDocumentInfo { id: string; key: string; title: string; @@ -28,29 +28,33 @@ export class CommentedDocumentsQuery { problem: string; curriculumDocs: CurriculumDocumentInfo[] = []; - studentDocs: StudentDocumentInfo[] = []; + userDocs: UserDocumentInfo[] = []; constructor( db: Firestore, - user: UserModelType, unit: string, problem: string) { makeAutoObservable(this); this.db = db; - this.user = user; this.unit = unit; this.problem = problem; } + setUser(user: UserModelType) { + this.user = user; + this.queryCurriculumDocs(); + this.queryUserDocs(); + } + getCurricumDocs(): CurriculumDocumentInfo[] { return this.curriculumDocs; } - getStudentDocs(): StudentDocumentInfo[] { - return this.studentDocs; + getUserDocs(): UserDocumentInfo[] { + return this.userDocs; } - async queryCurriculumDocs() { + private async queryCurriculumDocs() { const cDocsRef = this.db.collection("curriculum"); let docsQuery; if (this.user.network){ @@ -88,17 +92,19 @@ export class CommentedDocumentsQuery { })); } await Promise.all(promiseArr); - this.curriculumDocs = commentedDocs; + runInAction(() => { + this.curriculumDocs = commentedDocs; + }); } - async queryStudentDocs() { - console.log("running queryStudentDocs"); + private async queryUserDocs() { + console.log("running 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("network", "==", this.user.network).get()).docs + ? (await classesRef.where("networks", "array-contains", this.user.network).get()).docs : []; const allClasses = individualClasses.concat(networkClasses); console.log("teacher classes:", individualClasses, networkClasses); @@ -112,7 +118,7 @@ export class CommentedDocumentsQuery { // 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: StudentDocumentInfo[] = []; + const studentDocs: UserDocumentInfo[] = []; for (const group of teacherClassGroups) { const docsQuery = collection.where("context_id", "in", group); const result = await docsQuery.get(); @@ -126,7 +132,7 @@ export class CommentedDocumentsQuery { }); } } - const commentedDocs: StudentDocumentInfo[] = []; + const commentedDocs: UserDocumentInfo[] = []; const promiseArr: Promise[] = []; // TODO maybe combine multiple "docs" that have same ID? for (const doc of studentDocs){ @@ -140,7 +146,9 @@ export class CommentedDocumentsQuery { })); } await Promise.all(promiseArr); - this.studentDocs = commentedDocs; + runInAction(() => { + this.userDocs = commentedDocs; + }); } } From dc4517f0067d2427a8cf019b914d087291265a2f Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Tue, 20 Aug 2024 08:59:06 -0400 Subject: [PATCH 061/127] basic test of firestore onDocumentWritten function --- functions-v2/test/index.test.ts | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 functions-v2/test/index.test.ts diff --git a/functions-v2/test/index.test.ts b/functions-v2/test/index.test.ts new file mode 100644 index 0000000000..33b42b9127 --- /dev/null +++ b/functions-v2/test/index.test.ts @@ -0,0 +1,39 @@ +import initializeFFT from "firebase-functions-test"; +import * as logger from "firebase-functions/logger"; +import {updateClassDocNetworksOnUserChange} from "../src"; + +jest.mock("firebase-functions/logger"); + +// process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8080"; +const projectConfig = {projectId: "demo-test"}; +const fft = initializeFFT(projectConfig); + +describe("functions", () => { + beforeEach(async () => { + // await clearFirestoreData(projectConfig); + }); + + test("updateClassDocNetworksOnUserChange", async () => { + const wrapped = fft.wrap(updateClassDocNetworksOnUserChange); + + const beforeSnap = fft.firestore.makeDocumentSnapshot( + {foo: "bar"}, "demo/test/users/1234"); + const afterSnap = fft.firestore.makeDocumentSnapshot( + {foo: "bar2"}, "demo/test/users/1234"); + + const event = { + before: beforeSnap, + after: afterSnap, + params: { + root: "demo", + space: "test", + userId: "1234", + }, + }; + + await wrapped(event); + + expect(logger.info) + .toHaveBeenCalledWith("User updated", "demo/test/users/1234" ); + }); +}); From 7cceaebdb81453aba3db54be1f3c78719fd51a35 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Thu, 15 Aug 2024 18:33:16 -0400 Subject: [PATCH 062/127] feat: Permissions migration script to create/update Firestore class documents (PT-188108798) [#188108798](https://www.pivotaltracker.com/story/show/188108798) --- scripts/ai/download-documents-with-info.ts | 1 + scripts/ai/get-offering-info.ts | 137 ++++++++++++++++++--- scripts/ai/update-class-metadata.ts | 126 +++++++++++++++++++ scripts/lib/fetch-portal-entity.ts | 27 ++++ scripts/lib/script-utils.ts | 12 ++ 5 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 scripts/ai/update-class-metadata.ts create mode 100644 scripts/lib/fetch-portal-entity.ts diff --git a/scripts/ai/download-documents-with-info.ts b/scripts/ai/download-documents-with-info.ts index 1dc496b2b7..70819971a0 100644 --- a/scripts/ai/download-documents-with-info.ts +++ b/scripts/ai/download-documents-with-info.ts @@ -135,6 +135,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 091e53c228..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 count-docs.ts +// $ 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/update-class-metadata.ts b/scripts/ai/update-class-metadata.ts new file mode 100644 index 0000000000..23eab7774d --- /dev/null +++ b/scripts/ai/update-class-metadata.ts @@ -0,0 +1,126 @@ +#!/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(); + + if (documentSnapshots.empty) { + 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++; + } else { + // 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. + documentSnapshots.forEach(doc => { + 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 } + ]; + + for (const { field, expected, actual } of requiredMatches) { + if (expected !== actual) { + console.error(`Skipping update due to ${field} mismatch. Expected ${expected}, got ${actual}.`); + return; + } + } + + doc.ref.update({ name, networks, teachers } as any); + console.log(context_id, doc.id, "Updated existing class metadata with", { name, networks, teachers }); + metadataUpdated++; + }); + } + } +} + +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/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..693b97c474 100644 --- a/scripts/lib/script-utils.ts +++ b/scripts/lib/script-utils.ts @@ -30,6 +30,18 @@ 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); } From e750e4dd8bec9d802cf9451e98a93925b31ae963 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 11:05:45 -0400 Subject: [PATCH 063/127] Checkpoint: set 'unit' to null for personal metadata documents. --- functions/src/shared.ts | 2 +- src/lib/db.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/src/shared.ts b/functions/src/shared.ts index f1bcb75c59..b07b6eaeec 100644 --- a/functions/src/shared.ts +++ b/functions/src/shared.ts @@ -118,7 +118,7 @@ export interface IDocumentMetadata { strategies?: string[]; investigation?: string; problem?: string; - unit?: string; + unit?: string|null; visibility?: string; } export function isDocumentMetadata(o: any): o is IDocumentMetadata { diff --git a/src/lib/db.ts b/src/lib/db.ts index 45e6cb204f..feb568272f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -429,6 +429,8 @@ export class DB { firestoreMetadata.investigation = investigationOrdinal; firestoreMetadata.problem = problemOrdinal; firestoreMetadata.unit = unitCode; + } else if (metadata.type === "personal") { + firestoreMetadata.unit = null; } const validateCommentableDocument = getFirebaseFunction("validateCommentableDocument_v1"); From 6db46ecaa33eddefa2f2f0894acc89a1d75935f9 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 11:10:09 -0400 Subject: [PATCH 064/127] Fix: default unit field to null for metadata documents. --- src/lib/db.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index feb568272f..97c3954ca9 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -420,6 +420,7 @@ export class DB { key: documentKey, properties: {}, uid: userContext.uid, + unit: null }; if ("offeringId" in metadata && metadata.offeringId != null) { const { investigation, problem, unit } = this.stores; @@ -429,8 +430,6 @@ export class DB { firestoreMetadata.investigation = investigationOrdinal; firestoreMetadata.problem = problemOrdinal; firestoreMetadata.unit = unitCode; - } else if (metadata.type === "personal") { - firestoreMetadata.unit = null; } const validateCommentableDocument = getFirebaseFunction("validateCommentableDocument_v1"); From ce78985af2a685b91718f6dda7fbe4ba45ead302 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 11:23:40 -0400 Subject: [PATCH 065/127] Checkpoint: update firestore metadata when personal document title changes. --- src/hooks/use-document-sync-to-firebase.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/use-document-sync-to-firebase.ts b/src/hooks/use-document-sync-to-firebase.ts index 4a4eaf747c..cc65458819 100644 --- a/src/hooks/use-document-sync-to-firebase.ts +++ b/src/hooks/use-document-sync-to-firebase.ts @@ -127,7 +127,8 @@ export function useDocumentSyncToFirebase( onError: (err, title) => { console.warn(`ERROR: Failed to update document title for ${type} document ${key}:`, title); } - } + }, + additionalMutation: syncFirestoreDocumentProp }); // sync properties for problem, personal, and learning log documents From 704f5967a8627ffbbce740758ac75364c1368222 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 14:21:43 -0400 Subject: [PATCH 066/127] Add title property when personal/learning log metadata docs are created. --- src/lib/db-types.ts | 1 + src/lib/db.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/db-types.ts b/src/lib/db-types.ts index 844f60b87e..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; diff --git a/src/lib/db.ts b/src/lib/db.ts index 45e6cb204f..7370edaa57 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -437,8 +437,8 @@ export class DB { } } - public async createDocument(params: { type: DBDocumentType, content?: string }) { - const { type, content } = params; + 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) => { @@ -461,7 +461,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: @@ -542,7 +542,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); @@ -695,7 +696,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 = { From 53abd952e8bb4f73f6110dd53531bd32a1aa715b Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 14:48:05 -0400 Subject: [PATCH 067/127] Update metadata document strategies when teacher adds comment with strategy. --- src/hooks/document-comment-hooks.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index e1d495a8d5..1223c6c789 100644 --- a/src/hooks/document-comment-hooks.ts +++ b/src/hooks/document-comment-hooks.ts @@ -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,26 @@ 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) { + const metadataQuery = firestore.collection("documents").where("key", "==", documentKey); + metadataQuery.get().then(querySnapshot => { + querySnapshot.docs.forEach(doc => { + const docRef = doc.ref; + const docStrategies = doc.get("strategies") || []; + tags.forEach(tag => { + if (!docStrategies.includes(tag)) { + docStrategies.push(tag); + } + }); + docRef.update({ strategies: docStrategies }); + }); + }); + } + // 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; From 8356c59c46236908d47074d4827dabf898c425ab Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 20 Aug 2024 16:34:22 -0400 Subject: [PATCH 068/127] feat: add metadata info to existing documents in Portal Launches (PT-187971982) [#187971982](https://www.pivotaltracker.com/story/show/187971982) --- scripts/ai/offering-json-to-csv.ts | 52 ++++++++++++++++++ scripts/ai/update-metadata.ts | 86 ++++++++++++++++++------------ scripts/lib/script-utils.ts | 8 +++ 3 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 scripts/ai/offering-json-to-csv.ts diff --git a/scripts/ai/offering-json-to-csv.ts b/scripts/ai/offering-json-to-csv.ts new file mode 100644 index 0000000000..b1420559ab --- /dev/null +++ b/scripts/ai/offering-json-to-csv.ts @@ -0,0 +1,52 @@ +import fs from "fs"; + +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")); + +// 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; + } +} + +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-metadata.ts b/scripts/ai/update-metadata.ts index c7482c2e8d..039b910c57 100644 --- a/scripts/ai/update-metadata.ts +++ b/scripts/ai/update-metadata.ts @@ -11,12 +11,13 @@ 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, getProblemDetailsFromUrl, getScriptRootFilePath } from "../lib/script-utils.js"; +import { getUnitParam, getUnitCode } from "./offering-json-to-csv.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 ***`); @@ -56,10 +57,10 @@ function getNetworkInfo() { const { portal, demo } = getNetworkInfo(); // For now, only run for demo spaces -if (!demo) { - console.error("demo not defined, exiting"); - process.exit(1); -} +// if (!demo) { +// console.error("demo not defined, exiting"); +// process.exit(1); +// } console.log(`***** Reading doc and updating metadata *****`); const collectionUrl = getFirestoreBasePath(portal, demo); @@ -105,7 +106,7 @@ async function processFile(file: string) { } } - const annotations = documentContent?.annotations || []; + 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 @@ -121,35 +122,54 @@ 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 offeringInfoFile = `${sourcePath}/offering-info.json`; + const offeringInfo = JSON.parse(fs.readFileSync(offeringInfoFile, "utf8")); + + const offering = offeringInfo[offeringId]; + if (offering) { + const { activity_url } = offering; + const unitParam = getUnitParam(activity_url); + const unitCode = getUnitCode(unitParam); + const { investigation, problem } = getProblemDetailsFromUrl(activity_url); + + unitFields = { + problem, + investigation, + unit: unitCode + }; } + } 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 @@ -282,5 +302,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/lib/script-utils.ts b/scripts/lib/script-utils.ts index 693b97c474..21a70d3a57 100644 --- a/scripts/lib/script-utils.ts +++ b/scripts/lib/script-utils.ts @@ -45,3 +45,11 @@ export function getFirestoreClassesPath(portal: string, demo?: string | boolean) export function getScriptRootFilePath(filename: string) { return path.resolve(scriptsRoot, filename); } + +export function getProblemDetailsFromUrl(url: string) { + const urlParams = new URLSearchParams(url); + const unit = urlParams.get("unit"); + const investigationAndProblem = urlParams.get("problem"); + const [investigation, problem] = investigationAndProblem ? investigationAndProblem.split(".") : [null, null]; + return { unit, investigation, problem }; +} From 540f55865a7bb48604ad996813ee42acadacc914 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 18:06:23 -0400 Subject: [PATCH 069/127] Only update doc if comment has tags. --- src/hooks/document-comment-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index 1223c6c789..d0936226b3 100644 --- a/src/hooks/document-comment-hooks.ts +++ b/src/hooks/document-comment-hooks.ts @@ -125,7 +125,7 @@ export const usePostDocumentComment = (options?: PostDocumentCommentUseMutationO // update metadata document with the new tags const tags = comment.tags || []; const documentKey = isDocumentMetadata(document) ? document.key : undefined; - if (documentKey) { + if (documentKey && tags.length > 0) { const metadataQuery = firestore.collection("documents").where("key", "==", documentKey); metadataQuery.get().then(querySnapshot => { querySnapshot.docs.forEach(doc => { From ec3fbf8f791ab49b3532f2afc8389949218df8b3 Mon Sep 17 00:00:00 2001 From: lublagg Date: Tue, 20 Aug 2024 18:40:33 -0400 Subject: [PATCH 070/127] Use arrayUnion instead of getting doc strategies. --- src/hooks/document-comment-hooks.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index d0936226b3..9650798388 100644 --- a/src/hooks/document-comment-hooks.ts +++ b/src/hooks/document-comment-hooks.ts @@ -130,13 +130,7 @@ export const usePostDocumentComment = (options?: PostDocumentCommentUseMutationO metadataQuery.get().then(querySnapshot => { querySnapshot.docs.forEach(doc => { const docRef = doc.ref; - const docStrategies = doc.get("strategies") || []; - tags.forEach(tag => { - if (!docStrategies.includes(tag)) { - docStrategies.push(tag); - } - }); - docRef.update({ strategies: docStrategies }); + docRef.update({ strategies: firebase.firestore.FieldValue.arrayUnion(tags) }); }); }); } From 47891813dae8c6e9b5f4551fc24568b61b9d5efe Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Tue, 20 Aug 2024 19:58:54 -0400 Subject: [PATCH 071/127] chore: refactor and clean up --- scripts/ai/offering-json-to-csv.ts | 34 +------------------------ scripts/ai/update-metadata.ts | 15 +++-------- scripts/lib/script-utils.ts | 41 +++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/scripts/ai/offering-json-to-csv.ts b/scripts/ai/offering-json-to-csv.ts index b1420559ab..0e07f4e1e7 100644 --- a/scripts/ai/offering-json-to-csv.ts +++ b/scripts/ai/offering-json-to-csv.ts @@ -1,4 +1,5 @@ import fs from "fs"; +import { getClueBranch, getUnitParam, getUnitBranch, getUnitCode } from "../lib/script-utils.js"; import { datasetPath } from "./script-constants.js"; const sourceDirectory = "dataset1724085367882"; @@ -6,39 +7,6 @@ const sourcePath = `${datasetPath}${sourceDirectory}`; const offeringInfoFile = `${sourcePath}/offering-info.json`; const offeringInfo = JSON.parse(fs.readFileSync(offeringInfoFile, "utf8")); -// 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; - } -} - 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; diff --git a/scripts/ai/update-metadata.ts b/scripts/ai/update-metadata.ts index 039b910c57..c18b3a68e1 100644 --- a/scripts/ai/update-metadata.ts +++ b/scripts/ai/update-metadata.ts @@ -11,8 +11,7 @@ import fs from "fs"; import admin from "firebase-admin"; import { datasetPath, networkFileName } from "./script-constants.js"; -import { getFirestoreBasePath, getProblemDetailsFromUrl, getScriptRootFilePath } from "../lib/script-utils.js"; -import { getUnitParam, getUnitCode } from "./offering-json-to-csv.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. @@ -56,12 +55,6 @@ 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} ***`); @@ -129,14 +122,12 @@ async function processFile(file: string) { const offering = offeringInfo[offeringId]; if (offering) { const { activity_url } = offering; - const unitParam = getUnitParam(activity_url); - const unitCode = getUnitCode(unitParam); - const { investigation, problem } = getProblemDetailsFromUrl(activity_url); + const { investigation, problem, unit } = getProblemDetails(activity_url); unitFields = { problem, investigation, - unit: unitCode + unit }; } } else { diff --git a/scripts/lib/script-utils.ts b/scripts/lib/script-utils.ts index 21a70d3a57..b5f08470fe 100644 --- a/scripts/lib/script-utils.ts +++ b/scripts/lib/script-utils.ts @@ -46,10 +46,45 @@ export function getScriptRootFilePath(filename: string) { return path.resolve(scriptsRoot, filename); } -export function getProblemDetailsFromUrl(url: string) { +// 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 urlParams = new URLSearchParams(url); - const unit = urlParams.get("unit"); + 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 { unit, investigation, problem }; + return { investigation, problem, unit }; } From a7b5afecee8cb8165090c25e954ed710675cfc41 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Wed, 21 Aug 2024 06:27:29 -0400 Subject: [PATCH 072/127] Example of creating Firestore docs in the test --- functions-v2/jest.config.js | 7 ++++++ functions-v2/test/index.test.ts | 44 ++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/functions-v2/jest.config.js b/functions-v2/jest.config.js index 4a5b465ecb..9c02ec3c77 100644 --- a/functions-v2/jest.config.js +++ b/functions-v2/jest.config.js @@ -2,3 +2,10 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; + +// 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"; diff --git a/functions-v2/test/index.test.ts b/functions-v2/test/index.test.ts index 33b42b9127..2b18cd1e0e 100644 --- a/functions-v2/test/index.test.ts +++ b/functions-v2/test/index.test.ts @@ -1,25 +1,37 @@ import initializeFFT from "firebase-functions-test"; +import { + clearFirestoreData, +} from "firebase-functions-test/lib/providers/firestore"; import * as logger from "firebase-functions/logger"; +import * as admin from "firebase-admin"; import {updateClassDocNetworksOnUserChange} from "../src"; jest.mock("firebase-functions/logger"); -// process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8080"; +process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088"; const projectConfig = {projectId: "demo-test"}; const fft = initializeFFT(projectConfig); +// const app = admin.initializeApp(projectConfig); +admin.initializeApp(projectConfig); + describe("functions", () => { beforeEach(async () => { - // await clearFirestoreData(projectConfig); + await clearFirestoreData(projectConfig); }); test("updateClassDocNetworksOnUserChange", async () => { const wrapped = fft.wrap(updateClassDocNetworksOnUserChange); - const beforeSnap = fft.firestore.makeDocumentSnapshot( - {foo: "bar"}, "demo/test/users/1234"); - const afterSnap = fft.firestore.makeDocumentSnapshot( - {foo: "bar2"}, "demo/test/users/1234"); + const beforeSnap = fft.firestore.makeDocumentSnapshot({ + uid: "1234", + type: "teacher", + }, "demo/test/users/1234"); + const afterSnap = fft.firestore.makeDocumentSnapshot({ + uid: "1234", + type: "teacher", + networks: ["test-network"], + }, "demo/test/users/1234"); const event = { before: beforeSnap, @@ -31,6 +43,26 @@ describe("functions", () => { }, }; + const classesCollection = admin.firestore().collection("demo/test/classes"); + const newDocRef = classesCollection.doc("testclass-1"); + + await newDocRef + .set({ + context_id: "testclass-1", + id: "1", + teachers: ["1234"], + uri: "https://example.concord.org/classes/1", + }); + + await classesCollection + .doc("testclass-2") + .set({ + context_id: "testclass-2", + id: "2", + teachers: ["1235"], + uri: "https://example.concord.org/classes/2", + }); + await wrapped(event); expect(logger.info) From f5d65f8906970a98de65d437905b976316bf788c Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Wed, 21 Aug 2024 12:23:54 -0400 Subject: [PATCH 073/127] fix reading of comments when there are none There are cases in the code where a teacher tries to read comments on a document that doesn't have a metadata document. This was causing an Firestore access error. Also this fixes an issue where the built in exists function from Firestore rules was being overwritten by our own function. --- firebase-test/src/documents-rules.test.ts | 9 +++++++++ firestore.rules | 16 ++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/firebase-test/src/documents-rules.test.ts b/firebase-test/src/documents-rules.test.ts index f5a1f27d32..7e7da5d409 100644 --- a/firebase-test/src/documents-rules.test.ts +++ b/firebase-test/src/documents-rules.test.ts @@ -540,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"] })); diff --git a/firestore.rules b/firestore.rules index 152ccef4ad..fba9473184 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 != ""); } @@ -259,7 +259,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 @@ -311,10 +311,14 @@ service cloud.firestore { allow delete: if isAuthedTeacher() && userIsResourceUser(); // teachers can read their own documents or other documents in their network allow read: if (isAuthed() && (resource == null || userOwnsDocument() || resourceInUserClass())) || - (isAuthedTeacher() && (userInResourceTeachers() || resourceInTeacherNetworks())) + (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 @@ -344,7 +348,7 @@ service cloud.firestore { // check if document is in user's class request.auth.token.class_hash == docData.context_id || // check whether the document's network corresponds to one of the users's networks - exists(docNetwork) && (docNetwork in getTeacherNetworks()) || + 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 @@ -356,7 +360,7 @@ service cloud.firestore { // check whether the teacher can access the document function teacherCanAccessDocument() { - return isAuthedTeacher() && userCanAccessDocument(); + return isAuthedTeacher() && (!exists(getDocumentPath()) || userCanAccessDocument()); } function isValidCommentCreateRequest() { From c2ebe31b858615680b2f1a1d88a1755c2aedf33a Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 21 Aug 2024 14:24:14 -0400 Subject: [PATCH 074/127] Sync database class docs even in demo/qa sites. Efficiency improvements for commented-docs --- src/components/app.tsx | 4 +- src/components/chat/commented-documents.tsx | 10 +-- src/lib/teacher-network.test.ts | 76 ++++++++++++++++----- src/lib/teacher-network.ts | 31 ++++++--- src/models/commented-documents.ts | 8 --- 5 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/components/app.tsx b/src/components/app.tsx index e474101108..2957e12c2f 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -129,9 +129,7 @@ export const authAndConnect = (stores: IStores, onQAClear?: (result: boolean, er 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"); diff --git a/src/components/chat/commented-documents.tsx b/src/components/chat/commented-documents.tsx index aafadf061b..ae786465d4 100644 --- a/src/components/chat/commented-documents.tsx +++ b/src/components/chat/commented-documents.tsx @@ -1,4 +1,5 @@ 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"; @@ -17,7 +18,8 @@ interface IProps { handleDocView: (() => void) | undefined; } -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(); @@ -37,7 +39,7 @@ export const CommentedDocuments: React.FC = ({user, handleDocView}) => { return (
{ - (commentedDocumentsQuery.getCurricumDocs()).map((doc, index) => { + (commentedDocumentsQuery.curriculumDocs).map((doc, index) => { const {navTab} = getTabsOfCurriculumDoc(doc.path); return (
= ({user, handleDocView}) => { }) } { - (commentedDocumentsQuery.getUserDocs()).map((doc, index) =>{ + (commentedDocumentsQuery.userDocs).map((doc, index) =>{ const sectionDoc = store.documents.getDocument(doc.key); const networkDoc = store.networkDocuments.getDocument(doc.key); if (sectionDoc){ @@ -94,7 +96,7 @@ export const CommentedDocuments: React.FC = ({user, handleDocView}) => { }
); -}; +}); interface JProps { diff --git a/src/lib/teacher-network.test.ts b/src/lib/teacher-network.test.ts index 5a8efef72a..04143fca2b 100644 --- a/src/lib/teacher-network.test.ts +++ b/src/lib/teacher-network.test.ts @@ -9,6 +9,7 @@ import { OfferingWithoutTeachers, syncClass, syncOffering, syncTeacherClassesAndOfferings } from "./teacher-network"; import { DB } from "./db"; +import { ClassModel, ClassModelType } from "../models/stores/class"; const mockStores = { appMode: "authed", @@ -61,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}`; @@ -80,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, @@ -171,7 +187,7 @@ describe("Teacher network functions", () => { data: () => fsClass1})); fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - const result = await syncClass(firestore, kPortalJWT, partClass1); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); expect(fetchMock).toHaveBeenCalledTimes(1); expect(mockDoc).toHaveBeenCalledTimes(2); expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); @@ -186,7 +202,7 @@ 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); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); expect(mockDoc).not.toHaveBeenCalled(); expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); @@ -197,7 +213,7 @@ 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); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); expect(mockDoc).not.toHaveBeenCalled(); expect(mockDocGet).not.toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); @@ -208,7 +224,7 @@ 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); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).not.toHaveBeenCalled(); @@ -219,7 +235,7 @@ 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); + const result = await syncClass(firestore, kPortalJWT, partClass1, kTeacherUserModel); expect(mockDoc).toHaveBeenCalledWith(oldClassDocPath); expect(mockDocGet).toHaveBeenCalled(); expect(mockDocSet).toHaveBeenCalledWith(fsClass1); @@ -299,29 +315,53 @@ 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 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 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); + await syncTeacherClassesAndOfferings(firestore, user, portalClass1Model, kPortalJWT); expect(mockDoc).toHaveBeenCalledTimes(1); expect(mockDocGet).toHaveBeenCalledTimes(1); expect(mockDocSet).toHaveBeenCalledTimes(1); @@ -332,7 +372,7 @@ describe("Teacher network functions", () => { mockDocGet.mockImplementation(() => { throw new MockFirestorePermissionsError(); }); fetchMock.mockResponseOnce(JSON.stringify(portalClass1)); const firestore = new Firestore(mockDB); - await syncTeacherClassesAndOfferings(firestore, completeTeacher, kPortalJWT); + 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 1c256751e3..6dd546fb64 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -1,5 +1,6 @@ import firebase from "firebase/app"; import { Optional } from "utility-types"; +import { ClassModelType } from "../models/stores/class"; import { UserModelType } from "../models/stores/user"; import { arraysEqualIgnoringOrder } from "../utilities/js-utils"; import { Firestore, isFirestorePermissionsError } from "./firestore"; @@ -49,9 +50,9 @@ 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; - const promises: Promise[] = []; // map portal offerings to classes @@ -63,12 +64,24 @@ 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], network)); + promises.push(syncClass(firestore, rawPortalJWT, userClasses[context_id], user, network)); }); - if (network) { + if (network && rawPortalJWT) { // synchronize the offerings user.portalClassOfferings.forEach(async offering => { const { @@ -122,12 +135,14 @@ async function createOrUpdateClassDoc( }); } -export async function syncClass(firestore: Firestore, rawPortalJWT: string, - aClass: ClassWithoutTeachers, addNetwork?: string) { +export async function syncClass(firestore: Firestore, rawPortalJWT: string|undefined, + aClass: ClassWithoutTeachers, user: UserModelType, addNetwork?: string) { const { uri, context_id } = aClass; const promises: Promise[] = []; - if (uri && context_id && rawPortalJWT) { - const teachers = await getClassTeachers(uri, rawPortalJWT); + 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) { diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts index f1283eb6fc..51849d5d69 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -46,14 +46,6 @@ export class CommentedDocumentsQuery { this.queryUserDocs(); } - getCurricumDocs(): CurriculumDocumentInfo[] { - return this.curriculumDocs; - } - - getUserDocs(): UserDocumentInfo[] { - return this.userDocs; - } - private async queryCurriculumDocs() { const cDocsRef = this.db.collection("curriculum"); let docsQuery; From 486661f9cc0bf8745a8afe83dfa38d8865f8b1be Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Wed, 21 Aug 2024 14:34:13 -0400 Subject: [PATCH 075/127] Remove extra console.logs --- src/lib/teacher-network.ts | 6 ------ src/models/commented-documents.ts | 3 --- 2 files changed, 9 deletions(-) diff --git a/src/lib/teacher-network.ts b/src/lib/teacher-network.ts index 6dd546fb64..67104c2fe7 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -116,16 +116,13 @@ async function createOrUpdateClassDoc( // Update existing doc const data = current.data() as ClassDocument; if (!arraysEqualIgnoringOrder(aClass.teachers, data.teachers)) { - console.log("updating teacher array:", data.teachers, aClass.teachers); await docRef.update({ teachers: aClass.teachers }); } if (addNetwork && !data.network?.includes(addNetwork)) { - console.log("updating networks array:", data.network, addNetwork); await docRef.update({ network: firebase.firestore.FieldValue.arrayUnion(addNetwork) }); } } else { // Create the document. - console.log("new doc:", aClass, addNetwork); if (addNetwork) { await docRef.set({ ...aClass, network: addNetwork, networks: [addNetwork] }); } else { @@ -155,13 +152,10 @@ export async function syncClass(firestore: Firestore, rawPortalJWT: string|undef // Old location of the class document if (aClass.network) { - console.log('attempting to set old class doc:', `classes/${aClass.network}_${context_id}`, - classWithTeachers, addNetwork); promises.push(createOrUpdateClassDoc(firestore, `classes/${aClass.network}_${context_id}`, classWithTeachers, addNetwork)); } // New location of the class document - console.log('attempting to set new class doc:', `classes/${context_id}`, classWithTeachers, addNetwork); promises.push(createOrUpdateClassDoc(firestore, `classes/${context_id}`, classWithTeachers, addNetwork)); } return Promise.all(promises); diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts index 51849d5d69..543f6fca35 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -90,8 +90,6 @@ export class CommentedDocumentsQuery { } private async queryUserDocs() { - console.log("running queryUserDocs"); - // Find teacher's classes const classesRef = this.db.collection("classes"); const individualClasses = (await classesRef.where("teachers", "array-contains", this.user.id).get()).docs; @@ -99,7 +97,6 @@ export class CommentedDocumentsQuery { ? (await classesRef.where("networks", "array-contains", this.user.network).get()).docs : []; const allClasses = individualClasses.concat(networkClasses); - console.log("teacher classes:", individualClasses, networkClasses); const classIds = allClasses.map(doc => { return (doc.data() as ClassDocument).context_id; }); // Find student documents From 1185ea865442149ee4f5889de324c9fa0a897ea9 Mon Sep 17 00:00:00 2001 From: lublagg Date: Wed, 21 Aug 2024 14:38:53 -0400 Subject: [PATCH 076/127] Use spread operator with arrayUnion. --- src/hooks/document-comment-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/document-comment-hooks.ts b/src/hooks/document-comment-hooks.ts index 9650798388..2753b2d07a 100644 --- a/src/hooks/document-comment-hooks.ts +++ b/src/hooks/document-comment-hooks.ts @@ -130,7 +130,7 @@ export const usePostDocumentComment = (options?: PostDocumentCommentUseMutationO metadataQuery.get().then(querySnapshot => { querySnapshot.docs.forEach(doc => { const docRef = doc.ref; - docRef.update({ strategies: firebase.firestore.FieldValue.arrayUnion(tags) }); + docRef.update({ strategies: firebase.firestore.FieldValue.arrayUnion(...tags) }); }); }); } From 72354b749274e291b9e258fc0d1419f88fd7c2ca Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Thu, 22 Aug 2024 10:35:54 -0400 Subject: [PATCH 077/127] implement the actual updateClassDocNetworksOnUserChange --- functions-v2/.eslintrc.js | 2 + functions-v2/src/index.ts | 70 ++++++++- functions-v2/test/index.test.ts | 255 +++++++++++++++++++++++++------- 3 files changed, 276 insertions(+), 51 deletions(-) diff --git a/functions-v2/.eslintrc.js b/functions-v2/.eslintrc.js index d508d8596f..f5a0140ab3 100644 --- a/functions-v2/.eslintrc.js +++ b/functions-v2/.eslintrc.js @@ -31,5 +31,7 @@ module.exports = { "quotes": ["error", "double"], "import/no-unresolved": 0, "indent": ["error", 2], + "max-len": ["warn", {code: 120, ignoreUrls: true}], + "require-jsdoc": false, }, }; diff --git a/functions-v2/src/index.ts b/functions-v2/src/index.ts index a657da9f78..ec28b7fbd5 100644 --- a/functions-v2/src/index.ts +++ b/functions-v2/src/index.ts @@ -1,7 +1,75 @@ import {onDocumentWritten} from "firebase-functions/v2/firestore"; import * as logger from "firebase-functions/logger"; +import * as admin from "firebase-admin"; + +admin.initializeApp(); export const updateClassDocNetworksOnUserChange = - onDocumentWritten("{root}/{space}/users/{userId}", (event) => { + 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. 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} = classDoc.data() as {teachers: string[]}; + 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(); + classUpdatePromises.push( + classDoc.ref.update({networks: orderedNetworks}) + ); + }); + + await Promise.all(classUpdatePromises); + logger.info("User updated", event.document); }); diff --git a/functions-v2/test/index.test.ts b/functions-v2/test/index.test.ts index 2b18cd1e0e..9f273f72c9 100644 --- a/functions-v2/test/index.test.ts +++ b/functions-v2/test/index.test.ts @@ -4,7 +4,11 @@ import { } from "firebase-functions-test/lib/providers/firestore"; import * as logger from "firebase-functions/logger"; import * as admin from "firebase-admin"; -import {updateClassDocNetworksOnUserChange} from "../src"; + +// We cannot import the function here because we need to call +// initializeFFT first in order to set things up before the +// initializeApp is called in the function module. +// import {updateClassDocNetworksOnUserChange} from "../src"; jest.mock("firebase-functions/logger"); @@ -12,60 +16,211 @@ process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088"; const projectConfig = {projectId: "demo-test"}; const fft = initializeFFT(projectConfig); -// const app = admin.initializeApp(projectConfig); -admin.initializeApp(projectConfig); +// We can't initialize the firebase admin here because that +// can only happen once and the function module needs to do it. +// admin.initializeApp(projectConfig); + +type CollectionRef = admin.firestore.CollectionReference< + admin.firestore.DocumentData, admin.firestore.DocumentData +>; describe("functions", () => { beforeEach(async () => { await clearFirestoreData(projectConfig); }); - test("updateClassDocNetworksOnUserChange", async () => { - const wrapped = fft.wrap(updateClassDocNetworksOnUserChange); - - const beforeSnap = fft.firestore.makeDocumentSnapshot({ - uid: "1234", - type: "teacher", - }, "demo/test/users/1234"); - const afterSnap = fft.firestore.makeDocumentSnapshot({ - uid: "1234", - type: "teacher", - networks: ["test-network"], - }, "demo/test/users/1234"); - - const event = { - before: beforeSnap, - after: afterSnap, - params: { - root: "demo", - space: "test", - userId: "1234", - }, - }; - - const classesCollection = admin.firestore().collection("demo/test/classes"); - const newDocRef = classesCollection.doc("testclass-1"); - - await newDocRef - .set({ - context_id: "testclass-1", - id: "1", - teachers: ["1234"], - uri: "https://example.concord.org/classes/1", - }); - - await classesCollection - .doc("testclass-2") - .set({ - context_id: "testclass-2", - id: "2", - teachers: ["1235"], - uri: "https://example.concord.org/classes/2", - }); - - await wrapped(event); - - expect(logger.info) - .toHaveBeenCalledWith("User updated", "demo/test/users/1234" ); + describe("updateClassDocNetworksOnUserChange", () => { + let classesCollection: CollectionRef; + let usersCollection: CollectionRef; + + function init() { + 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 () => { + // The function module has to be imported after initializeFFT is called. + // The initializeFFT sets up environment vars so the + // admin.initializeApp() in index.ts will have the same project + // settings. + const {updateClassDocNetworksOnUserChange} = await import("../src"); + + // We can't use beforeEach because this needs to happen after the import. + // And we can't put the import in beforeEach because it would be hard to + // get the imported function typed properly. + init(); + const wrapped = fft.wrap(updateClassDocNetworksOnUserChange); + + 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 {updateClassDocNetworksOnUserChange} = await import("../src"); + init(); + const wrapped = fft.wrap(updateClassDocNetworksOnUserChange); + + 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", + }, + ]); + }); }); }); From 0b8c589f6e5baaf93b60227eff80235f3be74e7b Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Thu, 22 Aug 2024 10:42:31 -0400 Subject: [PATCH 078/127] Add test --- src/models/commented-documents.test.ts | 288 +++++++++++++++++++++++++ src/models/commented-documents.ts | 7 +- 2 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 src/models/commented-documents.test.ts 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 index 543f6fca35..1a8b3fd9de 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -40,10 +40,11 @@ export class CommentedDocumentsQuery { this.problem = problem; } - setUser(user: UserModelType) { + async setUser(user: UserModelType) { this.user = user; - this.queryCurriculumDocs(); - this.queryUserDocs(); + return Promise.all([ + this.queryCurriculumDocs(), + this.queryUserDocs()]); } private async queryCurriculumDocs() { From 7db9352e6194b115d79f0ec64d46da2a5493ec7a Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Thu, 22 Aug 2024 11:12:58 -0400 Subject: [PATCH 079/127] Missed this file before. --- src/models/document/document-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/document/document-utils.ts b/src/models/document/document-utils.ts index 59257ce4d6..0712c82db8 100644 --- a/src/models/document/document-utils.ts +++ b/src/models/document/document-utils.ts @@ -1,5 +1,5 @@ import { getParent } from "mobx-state-tree"; -import { IDocumentMetadata } from "../../../functions/src/shared"; +import { IDocumentMetadata } from "../../../shared/shared"; import { ProblemModelType } from "../curriculum/problem"; import { SectionModelType } from "../curriculum/section"; import { getSectionPath } from "../curriculum/unit"; From 781c57c5b61088833e1715f39195261c8a569c7a Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Thu, 22 Aug 2024 11:26:45 -0400 Subject: [PATCH 080/127] fix runtime jest tests --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 71ff14d4e1..7a98616acd 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?$": [ From d708403cde2950cfc6e2dc73696948faca83b0f1 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Thu, 22 Aug 2024 21:25:30 -0400 Subject: [PATCH 081/127] clean up the testing library --- functions-v2/test/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/functions-v2/test/index.test.ts b/functions-v2/test/index.test.ts index 9f273f72c9..a814dc64f1 100644 --- a/functions-v2/test/index.test.ts +++ b/functions-v2/test/index.test.ts @@ -223,4 +223,8 @@ describe("functions", () => { ]); }); }); + + afterAll(() => { + fft.cleanup(); + }); }); From 33b8750965413f9fe3fe8c48dcbdaf797b42f303 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Thu, 22 Aug 2024 21:42:22 -0400 Subject: [PATCH 082/127] handle duplicate class ids, and fix networks property --- src/lib/firestore-schema.ts | 3 ++- src/lib/teacher-network.ts | 11 +++++++++-- src/models/commented-documents.ts | 9 ++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/lib/firestore-schema.ts b/src/lib/firestore-schema.ts index 11d8447323..5bc64f31ae 100644 --- a/src/lib/firestore-schema.ts +++ b/src/lib/firestore-schema.ts @@ -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/teacher-network.ts b/src/lib/teacher-network.ts index 67104c2fe7..c61f6de59a 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -118,12 +118,19 @@ async function createOrUpdateClassDoc( if (!arraysEqualIgnoringOrder(aClass.teachers, data.teachers)) { await docRef.update({ teachers: aClass.teachers }); } - if (addNetwork && !data.network?.includes(addNetwork)) { - await docRef.update({ network: firebase.firestore.FieldValue.arrayUnion(addNetwork) }); + // 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); diff --git a/src/models/commented-documents.ts b/src/models/commented-documents.ts index 1a8b3fd9de..04147f9cee 100644 --- a/src/models/commented-documents.ts +++ b/src/models/commented-documents.ts @@ -98,16 +98,19 @@ export class CommentedDocumentsQuery { ? (await classesRef.where("networks", "array-contains", this.user.network).get()).docs : []; const allClasses = individualClasses.concat(networkClasses); - const classIds = allClasses.map(doc => { return (doc.data() as ClassDocument).context_id; }); + const classIds = new Set(); + allClasses.forEach(doc => { + classIds.add((doc.data() as ClassDocument).context_id); + }); // Find student documents - if (classIds.length === 0) { + 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 teacherClassGroups = chunk([...classIds], chunkSize); const studentDocs: UserDocumentInfo[] = []; for (const group of teacherClassGroups) { const docsQuery = collection.where("context_id", "in", group); From cdf50089c62fa8aca1ff4f055733ad16900a478e Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 23 Aug 2024 09:00:10 -0400 Subject: [PATCH 083/127] Improve some behavior using CSS --- src/components/annotations/annotation-arrow.scss | 2 +- .../annotations/annotation-button.scss | 2 +- src/components/annotations/annotation-node.scss | 4 ++-- src/components/annotations/arrow-annotation.scss | 16 ++++++++++++++-- src/components/annotations/arrow-annotation.tsx | 2 ++ src/components/document/annotation-layer.tsx | 12 ++++++++++-- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/annotations/annotation-arrow.scss b/src/components/annotations/annotation-arrow.scss index b87f837079..ecc7ed8363 100644 --- a/src/components/annotations/annotation-arrow.scss +++ b/src/components/annotations/annotation-arrow.scss @@ -19,7 +19,7 @@ stroke-width: 11px; } - &: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..8f480b8f1b 100644 --- a/src/components/annotations/arrow-annotation.scss +++ b/src/components/annotations/arrow-annotation.scss @@ -52,14 +52,26 @@ } } +// Only allow interacting with the drag handle when we see the 'show-handles' class. .drag-handle { cursor: pointer; + pointer-events: none; + + .annotation-layer.show-handles & { + pointer-events: auto; + } } .sparrow-delete-button { cursor: pointer; fill-opacity: 0; + pointer-events: none; + + .annotation-layer.show-handles & { + pointer-events: auto; + } + .sparrow-delete-button-front { // Allow hover and click to pass through to background 'highlight' element. pointer-events: none; @@ -92,7 +104,7 @@ } } - &:hover { + .annotation-layer.show-handles &:hover { .sparrow-delete-button-highlight { fill-opacity: .125; } @@ -123,7 +135,7 @@ } } -.visible-delete-button { +.annotation-layer.show-handles .visible-delete-button { .sparrow-delete-button-front { fill: vars.$annotation-half-blue; fill-opacity: 1; diff --git a/src/components/annotations/arrow-annotation.tsx b/src/components/annotations/arrow-annotation.tsx index b347bdf9f3..e5776f47c9 100644 --- a/src/components/annotations/arrow-annotation.tsx +++ b/src/components/annotations/arrow-annotation.tsx @@ -35,6 +35,7 @@ function DragHandle({ handleMouseDown(e, dragTarget)} + onClick={e => console.log('click drag handle')} > , _dragType: DragType) { + console.log('handle handleMouseDown'); if (!canEdit) return; setDragX(e.clientX); diff --git a/src/components/document/annotation-layer.tsx b/src/components/document/annotation-layer.tsx index 3b048bcf6e..f8f44bc992 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) { @@ -79,6 +84,7 @@ export const AnnotationLayer = observer(function AnnotationLayer({ // Clicking to select annotations function handleArrowClick(arrowId: string, event: MouseEvent) { + console.log("handleArrowClick"); if (readOnly) return; event.stopPropagation(); const annotation = content?.annotations.get(arrowId); @@ -320,9 +326,10 @@ export const AnnotationLayer = observer(function AnnotationLayer({ }; const handleAnnotationButtonClick = (e: React.MouseEvent, tileId: string, objectId: string, objectType?: string) => { + console.log("handleAnnotationButtonClick"); // 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 +362,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 (
Date: Fri, 23 Aug 2024 11:37:15 -0400 Subject: [PATCH 084/127] fix eslint --- functions-v2/.eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions-v2/.eslintrc.js b/functions-v2/.eslintrc.js index f5a0140ab3..910cb16d6b 100644 --- a/functions-v2/.eslintrc.js +++ b/functions-v2/.eslintrc.js @@ -32,6 +32,6 @@ module.exports = { "import/no-unresolved": 0, "indent": ["error", 2], "max-len": ["warn", {code: 120, ignoreUrls: true}], - "require-jsdoc": false, + "require-jsdoc": 0, }, }; From 2245070df8dafca5ad41efe51a4d497dfe4b8f61 Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Fri, 23 Aug 2024 12:08:36 -0400 Subject: [PATCH 085/127] only teachers can sync classes and offerings --- README.md | 1 + src/lib/db.ts | 4 ++++ src/lib/debug.ts | 1 + src/lib/teacher-network.ts | 3 +++ 4 files changed, 9 insertions(+) diff --git a/README.md b/README.md index fbf1d46a74..b6fd05b9dd 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,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 diff --git a/src/lib/db.ts b/src/lib/db.ts index adf3c49454..c9ad8a2ca3 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -40,6 +40,7 @@ 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 { @@ -115,6 +116,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!"); 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/teacher-network.ts b/src/lib/teacher-network.ts index c61f6de59a..e958a47f00 100644 --- a/src/lib/teacher-network.ts +++ b/src/lib/teacher-network.ts @@ -55,6 +55,9 @@ export function syncTeacherClassesAndOfferings( const { network } = user; 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 => { From cd2ec4c8ba57c1fcf5e6260f529afc8d9fbe148c Mon Sep 17 00:00:00 2001 From: Scott Cytacki Date: Fri, 23 Aug 2024 15:14:53 -0400 Subject: [PATCH 086/127] add test for non teacher --- src/lib/teacher-network.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/teacher-network.test.ts b/src/lib/teacher-network.test.ts index 04143fca2b..b988ea6895 100644 --- a/src/lib/teacher-network.test.ts +++ b/src/lib/teacher-network.test.ts @@ -3,7 +3,7 @@ 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 @@ -315,6 +315,15 @@ describe("Teacher network functions", () => { const completeTeacher = UserModel.create({ id: kTeacher1Id, type: "teacher", network: "test-network", portalClassOfferings: [userOffering1(), userOffering2()] }); + 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); From 84ee2be935d742e1c665430cde7a33861e5f785d Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 23 Aug 2024 15:56:14 -0400 Subject: [PATCH 087/127] Allow endpoint of straight sparrow to be on an arrow --- src/components/annotations/annotation-arrow.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/annotations/annotation-arrow.scss b/src/components/annotations/annotation-arrow.scss index ecc7ed8363..8283be16ca 100644 --- a/src/components/annotations/annotation-arrow.scss +++ b/src/components/annotations/annotation-arrow.scss @@ -17,6 +17,11 @@ .arrow-stem { stroke-opacity: 0; stroke-width: 11px; + pointer-events: none; + + .annotation-layer.show-handles & { + pointer-events: visible; + } } .annotation-layer.show-handles &:hover { From f06d3599d2a6c28ae7d160bd9debfbc6cb241314 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Fri, 23 Aug 2024 17:26:28 -0400 Subject: [PATCH 088/127] Handle long vs. short click --- .../tile_tests/arrow_annotation_spec.js | 4 +- .../annotations/arrow-annotation.tsx | 43 +++++++++++++------ src/components/document/annotation-layer.tsx | 29 ++++++++++++- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js index c01b95c26a..a272acb518 100644 --- a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js +++ b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js @@ -283,7 +283,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 +560,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/src/components/annotations/arrow-annotation.tsx b/src/components/annotations/arrow-annotation.tsx index e5776f47c9..930a3db27b 100644 --- a/src/components/annotations/arrow-annotation.tsx +++ b/src/components/annotations/arrow-annotation.tsx @@ -73,6 +73,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; @@ -85,7 +87,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) { @@ -112,6 +114,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"; @@ -186,14 +189,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) { - console.log('handle handleMouseDown'); if (!canEdit) return; setDragX(e.clientX); setDragY(e.clientY); setDragType(_dragType); + mouseDownTime.current = performance.now(); function handleMouseMove(e2: MouseEvent) { setClientX(e2.clientX); @@ -214,18 +219,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/document/annotation-layer.tsx b/src/components/document/annotation-layer.tsx index f8f44bc992..a38ce79a06 100644 --- a/src/components/document/annotation-layer.tsx +++ b/src/components/document/annotation-layer.tsx @@ -138,7 +138,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); @@ -325,8 +325,32 @@ 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) => { - console.log("handleAnnotationButtonClick"); // 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 (!showButtons) { @@ -412,6 +436,7 @@ export const AnnotationLayer = observer(function AnnotationLayer({ canEdit={!readOnly && editing} deleteArrow={(arrowId: string) => content?.deleteAnnotation(arrowId)} handleArrowClick={handleArrowClick} + handleDragHandleNonDrag={handleDragHandleNonDrag} documentBottom={documentBottom} documentLeft={documentLeft} documentRight={documentRight} From f44fc57765a06f9acc7b8375305c67bda5e80c6a Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 26 Aug 2024 11:32:56 -0400 Subject: [PATCH 089/127] Refactor CSS to avoid breaking non-sparrow elements with .drag-handle --- .../annotations/arrow-annotation.scss | 205 +++++++++--------- 1 file changed, 105 insertions(+), 100 deletions(-) diff --git a/src/components/annotations/arrow-annotation.scss b/src/components/annotations/arrow-annotation.scss index 8f480b8f1b..bc2c1ec07e 100644 --- a/src/components/annotations/arrow-annotation.scss +++ b/src/components/annotations/arrow-annotation.scss @@ -1,131 +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; + } + + &.can-edit { + &:hover { + cursor: pointer; + } } - } - &.dragging { - background-color: vars.$annotation-blue; - color: white; + &.dragging { + background-color: vars.$annotation-blue; + color: white; - &:hover { - outline: 4px solid vars.$annotation-blue-transparent; + &: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; + } } } } -} -// Only allow interacting with the drag handle when we see the 'show-handles' class. -.drag-handle { - cursor: pointer; - pointer-events: none; - - .annotation-layer.show-handles & { - pointer-events: auto; + // Only allow interacting with the drag handle when we see the 'show-handles' class. + .drag-handle { + pointer-events: none; } -} -.sparrow-delete-button { - cursor: pointer; - fill-opacity: 0; - - pointer-events: none; - - .annotation-layer.show-handles & { + &.show-handles .drag-handle { + cursor: pointer; pointer-events: auto; } - .sparrow-delete-button-front { - // Allow hover and click to pass through to background 'highlight' element. - pointer-events: none; - } - - .sparrow-delete-icon { - // Allow hover and click to pass through to background 'highlight' element. + .sparrow-delete-button { + fill-opacity: 0; pointer-events: none; - } - - .sparrow-delete-button-highlight { - fill: vars.$annotation-blue; - } - - .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; + // 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; } - } - .annotation-layer.show-handles &: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; } @@ -134,14 +150,3 @@ } } } - -.annotation-layer.show-handles .visible-delete-button { - .sparrow-delete-button-front { - fill: vars.$annotation-half-blue; - fill-opacity: 1; - } - - .sparrow-delete-icon { - fill-opacity: 1; - } -} From 95f699c4d1e3d661fca913c3fc980907658a0002 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 26 Aug 2024 16:46:41 -0400 Subject: [PATCH 090/127] Add tests for new "multiple sparrows" behaviors --- .../tile_tests/arrow_annotation_spec.js | 40 ++++++++++++++++--- .../support/elements/tile/ArrowAnnotation.js | 3 ++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js index a272acb518..9a06593069 100644 --- a/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js +++ b/cypress/e2e/functional/tile_tests/arrow_annotation_spec.js @@ -134,6 +134,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 +247,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 +277,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 }); 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`); } From f3a8fa99c9cd312e5173682e9d032b320f777970 Mon Sep 17 00:00:00 2001 From: Boris Goldowsky Date: Mon, 26 Aug 2024 17:50:03 -0400 Subject: [PATCH 091/127] Remove console.logs --- src/components/annotations/arrow-annotation.tsx | 1 - src/components/document/annotation-layer.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/annotations/arrow-annotation.tsx b/src/components/annotations/arrow-annotation.tsx index 930a3db27b..bbd228ff15 100644 --- a/src/components/annotations/arrow-annotation.tsx +++ b/src/components/annotations/arrow-annotation.tsx @@ -35,7 +35,6 @@ function DragHandle({ handleMouseDown(e, dragTarget)} - onClick={e => console.log('click drag handle')} > Date: Mon, 26 Aug 2024 18:39:27 -0400 Subject: [PATCH 092/127] Copy sample tile, add icons, add to QA unit. --- .../bar-graph/assets/bar-graph-icon.svg | 6 ++ .../bar-graph/bar-graph-content.test.ts | 19 ++++++ src/plugins/bar-graph/bar-graph-content.ts | 27 ++++++++ .../bar-graph/bar-graph-registration.ts | 23 +++++++ src/plugins/bar-graph/bar-graph-tile.test.tsx | 67 +++++++++++++++++++ src/plugins/bar-graph/bar-graph-tile.tsx | 26 +++++++ src/plugins/bar-graph/bar-graph-types.ts | 3 + src/plugins/bar-graph/bar-graph.scss | 17 +++++ src/public/demo/units/qa/content.json | 1 + src/register-tile-types.ts | 4 ++ 10 files changed, 193 insertions(+) create mode 100644 src/plugins/bar-graph/assets/bar-graph-icon.svg create mode 100644 src/plugins/bar-graph/bar-graph-content.test.ts create mode 100644 src/plugins/bar-graph/bar-graph-content.ts create mode 100644 src/plugins/bar-graph/bar-graph-registration.ts create mode 100644 src/plugins/bar-graph/bar-graph-tile.test.tsx create mode 100644 src/plugins/bar-graph/bar-graph-tile.tsx create mode 100644 src/plugins/bar-graph/bar-graph-types.ts create mode 100644 src/plugins/bar-graph/bar-graph.scss 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-content.test.ts b/src/plugins/bar-graph/bar-graph-content.test.ts new file mode 100644 index 0000000000..b17b04d442 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-content.test.ts @@ -0,0 +1,19 @@ +import { defaultBarGraphContent, BarGraphContentModel } from "./bar-graph-content"; + +describe("Bar Graph Content", () => { + it("has default content of 'hello world'", () => { + const content = defaultBarGraphContent(); + expect(content.text).toBe("Hello World"); + }); + + it("supports changing the text", () => { + const content = BarGraphContentModel.create(); + content.setText("New Text"); + expect(content.text).toBe("New Text"); + }); + + it("is always user resizable", () => { + const content = BarGraphContentModel.create(); + expect(content.isUserResizable).toBe(true); + }); +}); 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..8d2c5e77a4 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-content.ts @@ -0,0 +1,27 @@ +import { types, Instance } from "mobx-state-tree"; +import { TileContentModel } from "../../models/tiles/tile-content"; +import { kBarGraphTileType } from "./bar-graph-types"; + +export function defaultBarGraphContent(): BarGraphContentModelType { + return BarGraphContentModel.create({text: "Hello World"}); +} + + +export const BarGraphContentModel = TileContentModel + .named("BarGraphContentModel") + .props({ + type: types.optional(types.literal(kBarGraphTileType), kBarGraphTileType), + text: "", + }) + .views(self => ({ + get isUserResizable() { + return true; + } + })) + .actions(self => ({ + setText(text: string) { + self.text = text; + } + })); + +export interface BarGraphContentModelType extends Instance {} 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..8b0650a36c --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-tile.test.tsx @@ -0,0 +1,67 @@ +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "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"; + +describe("BarGraphToolComponent", () => { + 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} = + render(); + expect(getByText("Hello World")).toBeInTheDocument(); + }); + + it("updates the text when the model changes", async () => { + const {getByText, findByText} = + render(); + expect(getByText("Hello World")).toBeInTheDocument(); + + content.setText("New Text"); + + expect(await findByText("New Text")).toBeInTheDocument(); + }); + + it("updates the model when the user types", () => { + const {getByRole, getByText} = + render(); + expect(getByText("New Text")).toBeInTheDocument(); + + const textBox = getByRole("textbox"); + userEvent.type(textBox, "{selectall}{del}Typed Text"); + + expect(textBox).toHaveValue("Typed Text"); + expect(content.text).toBe("Typed Text"); + }); +}); 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..0ef6c31092 --- /dev/null +++ b/src/plugins/bar-graph/bar-graph-tile.tsx @@ -0,0 +1,26 @@ +import { observer } from "mobx-react"; +import React from "react"; +import { ITileProps } from "../../components/tiles/tile-component"; +import { BarGraphContentModelType } from "./bar-graph-content"; + +import "./bar-graph.scss"; + +export const BarGraphComponent: React.FC = observer((props) => { + // Note: capturing the content here and using it in handleChange() below may run the risk + // of encountering a stale closure issue depending on the order in which content changes, + // component renders, and calls to handleChange() occur. See the PR discussion at + // (https://github.com/concord-consortium/collaborative-learning/pull/1222/files#r824873678 + // and following comments) for details. We should be on the lookout for such issues. + const content = props.model.content as BarGraphContentModelType; + + const handleChange = (event: React.ChangeEvent) => { + content.setText(event.target.value); + }; + + return ( +
+