From 299a675a9c26ad1027ed1cc80cdc3776fc6a47fb Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 19 Nov 2024 15:52:57 -0800 Subject: [PATCH 01/30] IO-3000 Adjusted first approach at messaging WS changes. --- .../chat-affix/chat-affix.container.jsx | 14 +- .../registerMessagingSocketHandlers.js | 136 + .../chat-conversation-list.component.jsx | 57 +- .../chat-conversation-list.styles.scss | 25 +- .../chat-conversation.container.jsx | 58 +- .../chat-message-list.component.jsx | 151 +- .../chat-message-list.styles.scss | 21 +- .../chat-popup/chat-popup.component.jsx | 1 + client/src/contexts/SocketIO/useSocket.js | 10 +- client/src/graphql/conversations.queries.js | 30 + .../pages/manage/manage.page.component.jsx | 4 +- client/src/utils/fcm-handler.js | 134 +- client/vite.config.js | 26 - package-lock.json | 3234 ++++++----------- package.json | 32 +- server/graphql-client/queries.js | 87 + server/routes/miscellaneousRoutes.js | 54 + server/sms/receive.js | 307 +- server/sms/send.js | 34 +- server/sms/status.js | 63 +- server/utils/ioHelpers.js | 6 +- server/web-sockets/redisSocketEvents.js | 38 +- 22 files changed, 1952 insertions(+), 2570 deletions(-) create mode 100644 client/src/components/chat-affix/registerMessagingSocketHandlers.js diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 0541cd167..0c13d8327 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -2,20 +2,26 @@ import { useApolloClient } from "@apollo/client"; import { getToken, onMessage } from "@firebase/messaging"; import { Button, notification, Space } from "antd"; import axios from "axios"; -import React, { useEffect } from "react"; +import React, { useContext, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { messaging, requestForToken } from "../../firebase/firebase.utils"; import FcmHandler from "../../utils/fcm-handler"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; +import SocketContext from "../../contexts/SocketIO/socketContext"; +import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers"; export function ChatAffixContainer({ bodyshop, chatVisible }) { const { t } = useTranslation(); const client = useApolloClient(); + const { socket } = useContext(SocketContext); useEffect(() => { if (!bodyshop || !bodyshop.messagingservicesid) return; + //Register WS handlers + registerMessagingHandlers({ socket, client }); + async function SubscribeToTopic() { try { const r = await axios.post("/notifications/subscribe", { @@ -61,7 +67,11 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { SubscribeToTopic(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [bodyshop]); + + return () => { + unregisterMessagingHandlers({ socket }); + }; + }, [bodyshop, socket, t, client]); useEffect(() => { function handleMessage(payload) { diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js new file mode 100644 index 000000000..08d2e86bb --- /dev/null +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -0,0 +1,136 @@ +import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; + +export function registerMessagingHandlers({ socket, client }) { + if (!(socket && client)) return; + function handleNewMessageSummary(message) { + console.log("🚀 ~ SUMMARY CONSOLE LOG:", message); + + if (!message.isoutbound) { + //It's an inbound message. + if (!message.existingConversation) { + //Do a read query. + const queryResults = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: {} + }); + // Do a write query. Assume 0 unread messages to utilize code below. + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: {}, + data: { + conversations: [ + { ...message.newConversation, messages_aggregate: { aggregate: { count: 0 } } }, + ...queryResults + ] + } + }); + } + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: message.conversationId + }), + fields: { + updated_at: () => new Date(), + messages_aggregate(cached) { + return { aggregate: { count: cached.aggregate.count + 1 } }; + } + } + }); + client.cache.modify({ + fields: { + conversations(existingConversations = [], { readField }) { + return [ + { __ref: `conversations:${message.conversationId}` }, // TODO: This throws the cache merging error in apollo. + ...existingConversations.filter((c) => c.__ref !== `conversations:${message.conversationId}`) + ]; + } + } + }); + + client.cache.modify({ + fields: { + messages_aggregate(cached) { + return { aggregate: { count: cached.aggregate.count + 1 } }; + } + } + }); + } else { + //It's an outbound message + //Update the last updated for conversations in the list. If it's new, add it in. + // If it isn't just update the last updated at. + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: message.conversationId + }), + fields: { + updated_at: () => message.newMessage.updated_at + } + }); + } + } + + function handleNewMessageDetailed(message) { + console.log("🚀 ~ DETAIL CONSOLE LOG:", message); + //They're looking at the conversation right now. Need to merge into the list of messages i.e. append to the end. + //Add the message to the overall cache. + + //Handle outbound messages + if (message.newMessage.isoutbound) { + const queryResults = client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId: message.newMessage.conversationid } + }); + client.cache.writeQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId: message.newMessage.conversationid }, + data: { + ...queryResults, + conversations_by_pk: { + ...queryResults.conversations_by_pk, + messages: [...queryResults.conversations_by_pk.messages, message.newMessage] + } + } + }); + } + // We got this as a receive. + else { + } + } + + function handleMessageChanged(message) { + //Find it in the cache, and just update it based on what was sent. + client.cache.modify({ + id: client.cache.identify({ + __typename: "messages", + id: message.id + }), + fields: { + //TODO: see if there is a way to have this update all fields e.g. only spread in updates rather than prescribing + updated_at: () => new Date(), + status(cached) { + return message.status; + } + } + }); + } + + function handleConversationChanged(conversation) { + //If it was archived, marked unread, etc. + } + + socket.on("new-message-summary", handleNewMessageSummary); + socket.on("new-message-detailed", handleNewMessageDetailed); + socket.on("message-changed", handleMessageChanged); + socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. +} + +export function unregisterMessagingHandlers({ socket }) { + if (!socket) return; + socket.off("new-message-summary"); + socket.off("new-message-detailed"); + socket.off("message-changed"); + socket.off("message-changed"); + socket.off("conversation-changed"); +} diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index a49c3b4b4..38221085d 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,7 +1,7 @@ import { Badge, Card, List, Space, Tag } from "antd"; import React from "react"; import { connect } from "react-redux"; -import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized"; +import { Virtuoso } from "react-virtuoso"; import { createStructuredSelector } from "reselect"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; @@ -25,12 +25,7 @@ function ChatConversationListComponent({ setSelectedConversation, loadMoreConversations }) { - const cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 60 - }); - - const rowRenderer = ({ index, key, style, parent }) => { + const renderConversation = (index) => { const item = conversationList[index]; const cardContentRight = {item.updated_at}; const cardContentLeft = @@ -52,7 +47,8 @@ function ChatConversationListComponent({ )} ); - const cardExtra = ; + + const cardExtra = ; const getCardStyle = () => item.id === selectedConversation @@ -60,40 +56,27 @@ function ChatConversationListComponent({ : { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" }; return ( - - setSelectedConversation(item.id)} - style={style} - className={`chat-list-item - ${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`} - > - -
{cardContentLeft}
-
{cardContentRight}
-
-
-
+ setSelectedConversation(item.id)} + className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`} + > + +
{cardContentLeft}
+
{cardContentRight}
+
+
); }; return (
- - {({ height, width }) => ( - { - if (scrollTop + clientHeight === scrollHeight) { - loadMoreConversations(); - } - }} - /> - )} - + renderConversation(index)} + style={{ height: "100%", width: "100%" }} + endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom + />
); } diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss index 2922799a2..e6169777c 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss +++ b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss @@ -1,7 +1,7 @@ .chat-list-container { - overflow: hidden; - height: 100%; + height: 100%; /* Ensure it takes up the full available height */ border: 1px solid gainsboro; + overflow: auto; /* Allow scrolling for the Virtuoso component */ } .chat-list-item { @@ -14,3 +14,24 @@ color: #ff7a00; } } + +/* Virtuoso item container adjustments */ +.chat-list-container > div { + height: 100%; /* Ensure Virtuoso takes full height */ + display: flex; + flex-direction: column; +} + +/* Add spacing and better alignment for items */ +.chat-list-item { + padding: 0.5rem 0; /* Add spacing between list items */ + + .ant-card { + border-radius: 8px; /* Slight rounding for card edges */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Subtle shadow for better definition */ + } + + &:hover .ant-card { + border-color: #ff7a00; /* Highlight border on hover */ + } +} diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 2b91d2320..2ea760e3c 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,13 +1,14 @@ -import { useMutation, useQuery, useSubscription } from "@apollo/client"; -import React, { useState } from "react"; +import { useMutation, useQuery } from "@apollo/client"; +import axios from "axios"; +import React, { useEffect, useState, useContext } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; +import SocketContext from "../../contexts/SocketIO/socketContext"; +import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; -import ChatConversationComponent from "./chat-conversation.component"; -import axios from "axios"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import ChatConversationComponent from "./chat-conversation.component"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation, @@ -27,41 +28,34 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { nextFetchPolicy: "network-only" }); - const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, { - variables: { conversationId: selectedConversation } - }); + const { socket } = useContext(SocketContext); + + useEffect(() => { + socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + + return () => { + socket.emit("leave-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + }; + }, [selectedConversation, bodyshop, socket]); + + // const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, { + // variables: { conversationId: selectedConversation } + // }); const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); - const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, { - variables: { conversationId: selectedConversation }, - refetchQueries: ["UNREAD_CONVERSATION_COUNT"], - update(cache) { - cache.modify({ - id: cache.identify({ - __typename: "conversations", - id: selectedConversation - }), - fields: { - messages_aggregate(cached) { - return { aggregate: { count: 0 } }; - } - } - }); - } - }); - const unreadCount = - data && - data.messages && - data.messages.reduce((acc, val) => { + convoData && + convoData.conversations_by_pk && + convoData.conversations_by_pk.messages && + convoData.conversations_by_pk.messages.reduce((acc, val) => { return !val.read && !val.isoutbound ? acc + 1 : acc; }, 0); const handleMarkConversationAsRead = async () => { if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) { setMarkingAsReadInProgress(true); - await markConversationRead({}); + // await markConversationRead({}); await axios.post("/sms/markConversationRead", { conversationid: selectedConversation, imexshopid: bodyshop.imexshopid @@ -72,9 +66,9 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { return ( ); diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index 55aa81dc0..076941d8c 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -2,105 +2,90 @@ import Icon from "@ant-design/icons"; import { Tooltip } from "antd"; import i18n from "i18next"; import dayjs from "../../utils/day"; -import React, { useEffect, useRef } from "react"; +import React, { useRef, useEffect } from "react"; import { MdDone, MdDoneAll } from "react-icons/md"; -import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized"; +import { Virtuoso } from "react-virtuoso"; import { DateTimeFormatter } from "../../utils/DateFormatter"; import "./chat-message-list.styles.scss"; export default function ChatMessageListComponent({ messages }) { - const virtualizedListRef = useRef(null); + const virtuosoRef = useRef(null); - const _cache = new CellMeasurerCache({ - fixedWidth: true, - // minHeight: 50, - defaultHeight: 100 - }); + // Scroll to the bottom after a short delay when the component mounts + useEffect(() => { + const timer = setTimeout(() => { + if (virtuosoRef.current) { + virtuosoRef.current.scrollToIndex({ + index: messages.length - 1, + behavior: "auto" // Instantly scroll to the bottom + }); + } + }, 100); // Delay of 100ms to allow rendering + return () => clearTimeout(timer); // Cleanup the timer on unmount + }, [messages.length]); // Run only once on component mount - const scrollToBottom = (renderedrows) => { - //console.log("Scrolling to", messages.length); - // !!virtualizedListRef.current && - // virtualizedListRef.current.scrollToRow(messages.length); - // Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179 - //Scrolling does not work on this version of React. - }; - - useEffect(scrollToBottom, [messages]); - - const _rowRenderer = ({ index, key, parent, style }) => { + // Scroll to the bottom after the new messages are rendered + useEffect(() => { + if (virtuosoRef.current) { + // Allow the DOM and Virtuoso to fully render the new data + setTimeout(() => { + virtuosoRef.current.scrollToIndex({ + index: messages.length - 1, + align: "end", // Ensure the last message is fully visible + behavior: "smooth" // Smooth scrolling + }); + }, 50); // Slight delay to ensure layout recalculates + } + }, [messages]); // Triggered when new messages are added + //TODO: Does this one need to come into the render of the method? + const renderMessage = (index) => { + const message = messages[index]; return ( - - {({ measure, registerChild }) => ( -
-
- {MessageRender(messages[index])} - {StatusRender(messages[index].status)} +
+
+ +
+ {message.image_path && + message.image_path.map((i, idx) => ( +
+ + Received + +
+ ))} +
{message.text}
- {messages[index].isoutbound && ( -
- {i18n.t("messaging.labels.sentby", { - by: messages[index].userid, - time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a") - })} -
- )} +
+ {message.status && ( +
+ +
+ )} +
+ {message.isoutbound && ( +
+ {i18n.t("messaging.labels.sentby", { + by: message.userid, + time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a") + })}
)} - +
); }; return (
- - {({ height, width }) => ( - - )} - + renderMessage(index)} + followOutput="smooth" // Ensure smooth scrolling when new data is appended + style={{ height: "100%", width: "100%" }} + />
); } - -const MessageRender = (message) => { - return ( - -
- {message.image_path && - message.image_path.map((i, idx) => ( -
- - Received - -
- ))} -
{message.text}
-
-
- ); -}; - -const StatusRender = (status) => { - switch (status) { - case "sent": - return ; - case "delivered": - return ; - default: - return null; - } -}; diff --git a/client/src/components/chat-messages-list/chat-message-list.styles.scss b/client/src/components/chat-messages-list/chat-message-list.styles.scss index d576fa7b6..7958f02c3 100644 --- a/client/src/components/chat-messages-list/chat-message-list.styles.scss +++ b/client/src/components/chat-messages-list/chat-message-list.styles.scss @@ -1,37 +1,30 @@ .message-icon { - //position: absolute; - // bottom: 0rem; color: whitesmoke; border: #000000; position: absolute; margin: 0 0.1rem; bottom: 0.1rem; right: 0.3rem; - z-index: 5; } .chat { flex: 1; - //width: 300px; - //border: solid 1px #eee; display: flex; flex-direction: column; margin: 0.8rem 0rem; + overflow: hidden; // Ensure the content scrolls correctly } .messages { - //margin-top: 30px; display: flex; flex-direction: column; + padding: 0.5rem; // Add padding to avoid edge clipping } .message { border-radius: 20px; padding: 0.25rem 0.8rem; - //margin-top: 5px; - // margin-bottom: 5px; - //display: inline-block; .message-img { max-width: 10rem; @@ -56,7 +49,7 @@ position: relative; } -.yours .message.last:before { +.yours .message:last-child:before { content: ""; position: absolute; z-index: 0; @@ -68,7 +61,7 @@ border-bottom-right-radius: 15px; } -.yours .message.last:after { +.yours .message:last-child:after { content: ""; position: absolute; z-index: 1; @@ -88,12 +81,11 @@ color: white; margin-left: 25%; background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%); - background-attachment: fixed; position: relative; padding-bottom: 0.6rem; } -.mine .message.last:before { +.mine .message:last-child:before { content: ""; position: absolute; z-index: 0; @@ -102,11 +94,10 @@ height: 20px; width: 20px; background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%); - background-attachment: fixed; border-bottom-left-radius: 15px; } -.mine .message.last:after { +.mine .message:last-child:after { content: ""; position: absolute; z-index: 1; diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index d557ad91d..b9a500166 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -41,6 +41,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh const fcmToken = sessionStorage.getItem("fcmtoken"); + //TODO: Change to be a fallback incase sockets shit the bed useEffect(() => { if (fcmToken) { setpollInterval(0); diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 1c5a058fc..69c9402bd 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -3,10 +3,12 @@ import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; import { store } from "../../redux/store"; import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; +import { useDispatch } from "react-redux"; const useSocket = (bodyshop) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); + const dispatch = useDispatch(); useEffect(() => { const unsubscribe = auth.onIdTokenChanged(async (user) => { @@ -38,6 +40,8 @@ const useSocket = (bodyshop) => { case "alert-update": store.dispatch(addAlerts(message.payload)); break; + default: + break; } if (!import.meta.env.DEV) return; @@ -45,14 +49,12 @@ const useSocket = (bodyshop) => { }; const handleConnect = () => { - console.log("Socket connected:", socketInstance.id); socketInstance.emit("join-bodyshop-room", bodyshop.id); setClientId(socketInstance.id); store.dispatch(setWssStatus("connected")); }; const handleReconnect = (attempt) => { - console.log(`Socket reconnected after ${attempt} attempts`); store.dispatch(setWssStatus("connected")); }; @@ -62,10 +64,10 @@ const useSocket = (bodyshop) => { }; const handleDisconnect = () => { - console.log("Socket disconnected"); store.dispatch(setWssStatus("disconnected")); }; + //TODO: Check these handlers. socketInstance.on("connect", handleConnect); socketInstance.on("reconnect", handleReconnect); socketInstance.on("connect_error", handleConnectionError); @@ -89,7 +91,7 @@ const useSocket = (bodyshop) => { socketRef.current = null; } }; - }, [bodyshop]); + }, [bodyshop, dispatch]); return { socket: socketRef.current, clientId }; }; diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index f482cc325..e05a474ab 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -71,6 +71,17 @@ export const GET_CONVERSATION_DETAILS = gql` ro_number } } + messages(order_by: { created_at: asc_nulls_first }) { + id + status + text + isoutbound + image + image_path + userid + created_at + read + } } } `; @@ -114,3 +125,22 @@ export const UPDATE_CONVERSATION_LABEL = gql` } } `; + +export const GET_CONVERSATION_MESSAGES = gql` + query GET_CONVERSATION_MESSAGES($conversationId: uuid!) { + conversation: conversations_by_pk(id: $conversationId) { + id + phone_num + updated_at + label + } + messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) { + id + text + created_at + read + isoutbound + userid + } + } +`; diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 6dfd8af6e..c1a8e561c 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -145,7 +145,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { }; fetchAlerts(); - }, []); + }, [setAlerts]); // Use useEffect to watch for new alerts useEffect(() => { @@ -647,7 +647,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { return ( <> - {import.meta.env.PROD && } + {true && } diff --git a/client/src/utils/fcm-handler.js b/client/src/utils/fcm-handler.js index c6284c764..e3f7c8d43 100644 --- a/client/src/utils/fcm-handler.js +++ b/client/src/utils/fcm-handler.js @@ -1,70 +1,70 @@ export default async function FcmHandler({ client, payload }) { console.log("FCM", payload); - switch (payload.type) { - case "messaging-inbound": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: payload.conversationid - }), - fields: { - messages_aggregate(cached) { - return { aggregate: { count: cached.aggregate.count + 1 } }; - } - } - }); - client.cache.modify({ - fields: { - messages_aggregate(cached) { - return { aggregate: { count: cached.aggregate.count + 1 } }; - } - } - }); - break; - case "messaging-outbound": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: payload.conversationid - }), - fields: { - updated_at(oldupdated0) { - return new Date(); - } - // messages_aggregate(cached) { - // return { aggregate: { count: cached.aggregate.count + 1 } }; - // }, - } - }); - break; - case "messaging-mark-conversation-read": - let previousUnreadCount = 0; - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: payload.conversationid - }), - fields: { - messages_aggregate(cached) { - previousUnreadCount = cached.aggregate.count; - return { aggregate: { count: 0 } }; - } - } - }); - client.cache.modify({ - fields: { - messages_aggregate(cached) { - return { - aggregate: { - count: cached.aggregate.count - previousUnreadCount - } - }; - } - } - }); - break; - default: - console.log("No payload type set."); - break; - } + // switch (payload.type) { + // case "messaging-inbound": + // client.cache.modify({ + // id: client.cache.identify({ + // __typename: "conversations", + // id: payload.conversationid + // }), + // fields: { + // messages_aggregate(cached) { + // return { aggregate: { count: cached.aggregate.count + 1 } }; + // } + // } + // }); + // client.cache.modify({ + // fields: { + // messages_aggregate(cached) { + // return { aggregate: { count: cached.aggregate.count + 1 } }; + // } + // } + // }); + // break; + // case "messaging-outbound": + // client.cache.modify({ + // id: client.cache.identify({ + // __typename: "conversations", + // id: payload.conversationid + // }), + // fields: { + // updated_at(oldupdated0) { + // return new Date(); + // } + // // messages_aggregate(cached) { + // // return { aggregate: { count: cached.aggregate.count + 1 } }; + // // }, + // } + // }); + // break; + // case "messaging-mark-conversation-read": + // let previousUnreadCount = 0; + // client.cache.modify({ + // id: client.cache.identify({ + // __typename: "conversations", + // id: payload.conversationid + // }), + // fields: { + // messages_aggregate(cached) { + // previousUnreadCount = cached.aggregate.count; + // return { aggregate: { count: 0 } }; + // } + // } + // }); + // client.cache.modify({ + // fields: { + // messages_aggregate(cached) { + // return { + // aggregate: { + // count: cached.aggregate.count - previousUnreadCount + // } + // }; + // } + // } + // }); + // break; + // default: + // console.log("No payload type set."); + // break; + // } } diff --git a/client/vite.config.js b/client/vite.config.js index 03f56c538..7bb200e23 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -1,8 +1,5 @@ import react from "@vitejs/plugin-react"; import { promises as fsPromises } from "fs"; -import { createRequire } from "module"; -import * as path from "path"; -import * as url from "url"; import { createLogger, defineConfig } from "vite"; import { ViteEjsPlugin } from "vite-plugin-ejs"; import eslint from "vite-plugin-eslint"; @@ -18,28 +15,6 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { const getFormattedTimestamp = () => new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m."); -/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */ -const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`; - -function reactVirtualizedFix() { - return { - name: "flat:react-virtualized", - configResolved: async () => { - const require = createRequire(import.meta.url); - const reactVirtualizedPath = require.resolve("react-virtualized"); - const { pathname: reactVirtualizedFilePath } = new url.URL(reactVirtualizedPath, import.meta.url); - const file = reactVirtualizedFilePath.replace( - path.join("dist", "commonjs", "index.js"), - path.join("dist", "es", "WindowScroller", "utils", "onScroll.js") - ); - const code = await fsPromises.readFile(file, "utf-8"); - const modified = code.replace(WRONG_CODE, ""); - await fsPromises.writeFile(file, modified); - } - }; -} -/** End of hack */ - export const logger = createLogger("info", { allowClearScreen: false }); @@ -108,7 +83,6 @@ export default defineConfig({ gcm_sender_id: "103953800507" } }), - reactVirtualizedFix(), react(), eslint() ], diff --git a/package-lock.json b/package-lock.json index d95191db5..f1e6a5f31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.679.0", - "@aws-sdk/client-elasticache": "^3.675.0", - "@aws-sdk/client-s3": "^3.689.0", - "@aws-sdk/client-secrets-manager": "^3.675.0", - "@aws-sdk/client-ses": "^3.675.0", - "@aws-sdk/credential-provider-node": "^3.675.0", - "@opensearch-project/opensearch": "^2.12.0", + "@aws-sdk/client-cloudwatch-logs": "^3.693.0", + "@aws-sdk/client-elasticache": "^3.693.0", + "@aws-sdk/client-s3": "^3.693.0", + "@aws-sdk/client-secrets-manager": "^3.693.0", + "@aws-sdk/client-ses": "^3.693.0", + "@aws-sdk/credential-provider-node": "^3.693.0", + "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", "aws4": "^1.13.2", @@ -24,20 +24,20 @@ "bluebird": "^3.7.2", "body-parser": "^1.20.3", "canvas": "^2.11.2", - "chart.js": "^4.4.5", + "chart.js": "^4.4.6", "cloudinary": "^2.5.1", - "compression": "^1.7.4", + "compression": "^1.7.5", "cookie-parser": "^1.4.7", "cors": "2.8.5", "csrf": "^3.1.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", - "firebase-admin": "^12.6.0", + "firebase-admin": "^13.0.0", "graphql": "^16.9.0", "graphql-request": "^6.1.0", "inline-css": "^4.0.2", - "intuit-oauth": "^4.1.2", + "intuit-oauth": "^4.1.3", "ioredis": "^5.4.1", "json-2-csv": "^5.5.6", "lodash": "^4.17.21", @@ -46,18 +46,18 @@ "multer": "^1.4.5-lts.1", "node-mailjet": "^6.0.6", "node-persist": "^4.0.3", - "nodemailer": "^6.9.15", - "phone": "^3.1.51", + "nodemailer": "^6.9.16", + "phone": "^3.1.53", "recursive-diff": "^1.0.9", "redis": "^4.7.0", "rimraf": "^6.0.1", - "soap": "^1.1.5", - "socket.io": "^4.8.0", + "soap": "^1.1.6", + "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", "ssh2-sftp-client": "^10.0.3", "twilio": "^4.23.0", "uuid": "^10.0.0", - "winston": "^3.15.0", + "winston": "^3.17.0", "winston-cloudwatch": "^6.3.0", "xml2js": "^0.6.2", "xmlbuilder2": "^3.1.1" @@ -266,53 +266,53 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.679.0.tgz", - "integrity": "sha512-A1qTVNX8KdpqvXgULd4Suo88uuNWPa8DiuBL8Qkw/WefYT7TSWsOpwuVK0oFkMpCfB0rQN9fXZh2DBiaz8YmZg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.693.0.tgz", + "integrity": "sha512-ZJIzTqRSQmvBjGSYeE38mUO0Y70ioBAbokrIqQZdY7N+hQNU6xPWgZBURWdkC+yLfjCpbN9mSR4MPc/w6Tu+LQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.679.0", - "@aws-sdk/client-sts": "3.679.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.679.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.679.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/eventstream-serde-browser": "^3.0.10", - "@smithy/eventstream-serde-config-resolver": "^3.0.7", - "@smithy/eventstream-serde-node": "^3.0.9", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/eventstream-serde-browser": "^3.0.12", + "@smithy/eventstream-serde-config-resolver": "^3.0.9", + "@smithy/eventstream-serde-node": "^3.0.11", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", @@ -322,484 +322,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.679.0.tgz", - "integrity": "sha512-/0cAvYnpOZTo/Y961F1kx2fhDDLUYZ0SQQ5/75gh3xVImLj7Zw+vp74ieqFbqWLYGMaq8z1Arr9A8zG95mbLdg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.679.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.679.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.679.0.tgz", - "integrity": "sha512-/dBYWcCwbA/id4sFCIVZvf0UsvzHCC68SryxeNQk/PDkY9N4n5yRcMUkZDaEyQCjowc3kY4JOXp2AdUP037nhA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.679.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.679.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/client-sts": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.679.0.tgz", - "integrity": "sha512-3CvrT8w1RjFu1g8vKA5Azfr5V83r2/b68Ock43WE003Bq/5Y38mwmYX7vk0fPHzC3qejt4YMAWk/C3fSKOy25g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.679.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-node": "3.679.0", - "@aws-sdk/middleware-host-header": "3.679.0", - "@aws-sdk/middleware-logger": "3.679.0", - "@aws-sdk/middleware-recursion-detection": "3.679.0", - "@aws-sdk/middleware-user-agent": "3.679.0", - "@aws-sdk/region-config-resolver": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@aws-sdk/util-user-agent-browser": "3.679.0", - "@aws-sdk/util-user-agent-node": "3.679.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/core": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.679.0.tgz", - "integrity": "sha512-CS6PWGX8l4v/xyvX8RtXnBisdCa5+URzKd0L6GvHChype9qKUVxO/Gg6N/y43Hvg7MNWJt9FBPNWIxUB+byJwg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.679.0.tgz", - "integrity": "sha512-EdlTYbzMm3G7VUNAMxr9S1nC1qUNqhKlAxFU8E7cKsAe8Bp29CD5HAs3POc56AVo9GC4yRIS+/mtlZSmrckzUA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.679.0.tgz", - "integrity": "sha512-ZoKLubW5DqqV1/2a3TSn+9sSKg0T8SsYMt1JeirnuLJF0mCoYFUaWMyvxxKuxPoqvUsaycxKru4GkpJ10ltNBw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.679.0.tgz", - "integrity": "sha512-Rg7t8RwUzKcumpipG4neZqaeJ6DF+Bco1+FHn5BZB68jpvwvjBjcQUuWkxj18B6ctYHr1fkunnzeKEn/+vy7+w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.679.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.679.0.tgz", - "integrity": "sha512-E3lBtaqCte8tWs6Rkssc8sLzvGoJ10TLGvpkijOlz43wPd6xCRh1YLwg6zolf9fVFtEyUs/GsgymiASOyxhFtw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.679.0", - "@aws-sdk/credential-provider-http": "3.679.0", - "@aws-sdk/credential-provider-ini": "3.679.0", - "@aws-sdk/credential-provider-process": "3.679.0", - "@aws-sdk/credential-provider-sso": "3.679.0", - "@aws-sdk/credential-provider-web-identity": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.679.0.tgz", - "integrity": "sha512-u/p4TV8kQ0zJWDdZD4+vdQFTMhkDEJFws040Gm113VHa/Xo1SYOjbpvqeuFoz6VmM0bLvoOWjxB9MxnSQbwKpQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.679.0.tgz", - "integrity": "sha512-SAtWonhi9asxn0ukEbcE81jkyanKgqpsrtskvYPpO9Z9KOednM4Cqt6h1bfcS9zaHjN2zu815Gv8O7WiV+F/DQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.679.0", - "@aws-sdk/core": "3.679.0", - "@aws-sdk/token-providers": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.679.0.tgz", - "integrity": "sha512-a74tLccVznXCaBefWPSysUcLXYJiSkeUmQGtalNgJ1vGkE36W5l/8czFiiowdWdKWz7+x6xf0w+Kjkjlj42Ung==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.679.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.679.0.tgz", - "integrity": "sha512-y176HuQ8JRY3hGX8rQzHDSbCl9P5Ny9l16z4xmaiLo+Qfte7ee4Yr3yaAKd7GFoJ3/Mhud2XZ37fR015MfYl2w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-logger": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.679.0.tgz", - "integrity": "sha512-0vet8InEj7nvIvGKk+ch7bEF5SyZ7Us9U7YTEgXPrBNStKeRUsgwRm0ijPWWd0a3oz2okaEwXsFl7G/vI0XiEA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.679.0.tgz", - "integrity": "sha512-sQoAZFsQiW/LL3DfKMYwBoGjYDEnMbA9WslWN8xneCmBAwKo6IcSksvYs23PP8XMIoBGe2I2J9BSr654XWygTQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.679.0.tgz", - "integrity": "sha512-4hdeXhPDURPqQLPd9jCpUEo9fQITXl3NM3W1MwcJpE0gdUM36uXkQOYsTPeeU/IRCLVjK8Htlh2oCaM9iJrLCA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@aws-sdk/util-endpoints": "3.679.0", - "@smithy/core": "^2.4.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.679.0.tgz", - "integrity": "sha512-Ybx54P8Tg6KKq5ck7uwdjiKif7n/8g1x+V0V9uTjBjRWqaIgiqzXwKWoPj6NCNkE7tJNtqI4JrNxp/3S3HvmRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/token-providers": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.679.0.tgz", - "integrity": "sha512-1/+Zso/x2jqgutKixYFQEGli0FELTgah6bm7aB+m2FAWH4Hz7+iMUsazg6nSWm714sG9G3h5u42Dmpvi9X6/hA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.679.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/types": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", - "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.679.0.tgz", - "integrity": "sha512-YL6s4Y/1zC45OvddvgE139fjeWSKKPgLlnfrvhVL7alNyY9n7beR4uhoDpNrt5mI6sn9qiBF17790o+xLAXjjg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.679.0.tgz", - "integrity": "sha512-CusSm2bTBG1kFypcsqU8COhnYc6zltobsqs3nRrvYqYaOqtMnuE46K4XTWpnzKgwDejgZGOE+WYyprtAxrPvmQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.679.0", - "@smithy/types": "^3.5.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.679.0.tgz", - "integrity": "sha512-Bw4uXZ+NU5ed6TNfo4tBbhBSW+2eQxXYjYBGl5gLUNUpg2pDFToQAP6rXBFiwcG52V2ny5oLGiD82SoYuYkAVg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.679.0", - "@aws-sdk/types": "3.679.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -814,52 +336,52 @@ } }, "node_modules/@aws-sdk/client-elasticache": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.675.0.tgz", - "integrity": "sha512-OMdlxBrsrDdBw2/RHj80NSAbb4wQYR0TJMvSbuC/XYnF1W9OpNpNpzQ9ZdZwiLymkjQMgfrVk27FiEivEBwj1A==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.693.0.tgz", + "integrity": "sha512-ofdENKhzBcDjIRS6i7CDwvQetqTRn9v8BFFo1uqrmJVs5qLW6wuKDL7oI4ESXV2I1t9jJsobboKBj9naMxCciQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.675.0", - "@aws-sdk/client-sts": "3.675.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.675.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.6", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { @@ -867,610 +389,119 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.689.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.689.0.tgz", - "integrity": "sha512-qYD1GJEPeLM6H3x8BuAAMXZltvVce5vGiwtZc9uMkBBo3HyFnmPitIPTPfaD1q8LOn/7KFdkY4MJ4e8D3YpV9g==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.693.0.tgz", + "integrity": "sha512-vgGI2e0Q6pzyhqfrSysi+sk/i+Nl+lMon67oqj/57RcCw9daL1/inpS+ADuwHpiPWkrg+U0bOXnmHjkLeTslJg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.687.0", - "@aws-sdk/client-sts": "3.687.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/credential-provider-node": "3.687.0", - "@aws-sdk/middleware-bucket-endpoint": "3.686.0", - "@aws-sdk/middleware-expect-continue": "3.686.0", - "@aws-sdk/middleware-flexible-checksums": "3.689.0", - "@aws-sdk/middleware-host-header": "3.686.0", - "@aws-sdk/middleware-location-constraint": "3.686.0", - "@aws-sdk/middleware-logger": "3.686.0", - "@aws-sdk/middleware-recursion-detection": "3.686.0", - "@aws-sdk/middleware-sdk-s3": "3.687.0", - "@aws-sdk/middleware-ssec": "3.686.0", - "@aws-sdk/middleware-user-agent": "3.687.0", - "@aws-sdk/region-config-resolver": "3.686.0", - "@aws-sdk/signature-v4-multi-region": "3.687.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-endpoints": "3.686.0", - "@aws-sdk/util-user-agent-browser": "3.686.0", - "@aws-sdk/util-user-agent-node": "3.687.0", - "@aws-sdk/xml-builder": "3.686.0", - "@smithy/config-resolver": "^3.0.10", - "@smithy/core": "^2.5.1", - "@smithy/eventstream-serde-browser": "^3.0.11", - "@smithy/eventstream-serde-config-resolver": "^3.0.8", - "@smithy/eventstream-serde-node": "^3.0.10", - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/hash-blob-browser": "^3.1.7", - "@smithy/hash-node": "^3.0.8", - "@smithy/hash-stream-node": "^3.1.7", - "@smithy/invalid-dependency": "^3.0.8", - "@smithy/md5-js": "^3.0.8", - "@smithy/middleware-content-length": "^3.0.10", - "@smithy/middleware-endpoint": "^3.2.1", - "@smithy/middleware-retry": "^3.0.25", - "@smithy/middleware-serde": "^3.0.8", - "@smithy/middleware-stack": "^3.0.8", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/protocol-http": "^4.1.5", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-bucket-endpoint": "3.693.0", + "@aws-sdk/middleware-expect-continue": "3.693.0", + "@aws-sdk/middleware-flexible-checksums": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-location-constraint": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-sdk-s3": "3.693.0", + "@aws-sdk/middleware-ssec": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/signature-v4-multi-region": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@aws-sdk/xml-builder": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/eventstream-serde-browser": "^3.0.12", + "@smithy/eventstream-serde-config-resolver": "^3.0.9", + "@smithy/eventstream-serde-node": "^3.0.11", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-blob-browser": "^3.1.8", + "@smithy/hash-node": "^3.0.9", + "@smithy/hash-stream-node": "^3.1.8", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/md5-js": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.25", - "@smithy/util-defaults-mode-node": "^3.0.25", - "@smithy/util-endpoints": "^2.1.4", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-retry": "^3.0.8", - "@smithy/util-stream": "^3.2.1", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", + "@smithy/util-stream": "^3.3.0", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.7", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.687.0.tgz", - "integrity": "sha512-dfj0y9fQyX4kFill/ZG0BqBTLQILKlL7+O5M4F9xlsh2WNuV2St6WtcOg14Y1j5UODPJiJs//pO+mD1lihT5Kw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/middleware-host-header": "3.686.0", - "@aws-sdk/middleware-logger": "3.686.0", - "@aws-sdk/middleware-recursion-detection": "3.686.0", - "@aws-sdk/middleware-user-agent": "3.687.0", - "@aws-sdk/region-config-resolver": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-endpoints": "3.686.0", - "@aws-sdk/util-user-agent-browser": "3.686.0", - "@aws-sdk/util-user-agent-node": "3.687.0", - "@smithy/config-resolver": "^3.0.10", - "@smithy/core": "^2.5.1", - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/hash-node": "^3.0.8", - "@smithy/invalid-dependency": "^3.0.8", - "@smithy/middleware-content-length": "^3.0.10", - "@smithy/middleware-endpoint": "^3.2.1", - "@smithy/middleware-retry": "^3.0.25", - "@smithy/middleware-serde": "^3.0.8", - "@smithy/middleware-stack": "^3.0.8", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/protocol-http": "^4.1.5", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.25", - "@smithy/util-defaults-mode-node": "^3.0.25", - "@smithy/util-endpoints": "^2.1.4", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-retry": "^3.0.8", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.687.0.tgz", - "integrity": "sha512-Rdd8kLeTeh+L5ZuG4WQnWgYgdv7NorytKdZsGjiag1D8Wv3PcJvPqqWdgnI0Og717BSXVoaTYaN34FyqFYSx6Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/credential-provider-node": "3.687.0", - "@aws-sdk/middleware-host-header": "3.686.0", - "@aws-sdk/middleware-logger": "3.686.0", - "@aws-sdk/middleware-recursion-detection": "3.686.0", - "@aws-sdk/middleware-user-agent": "3.687.0", - "@aws-sdk/region-config-resolver": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-endpoints": "3.686.0", - "@aws-sdk/util-user-agent-browser": "3.686.0", - "@aws-sdk/util-user-agent-node": "3.687.0", - "@smithy/config-resolver": "^3.0.10", - "@smithy/core": "^2.5.1", - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/hash-node": "^3.0.8", - "@smithy/invalid-dependency": "^3.0.8", - "@smithy/middleware-content-length": "^3.0.10", - "@smithy/middleware-endpoint": "^3.2.1", - "@smithy/middleware-retry": "^3.0.25", - "@smithy/middleware-serde": "^3.0.8", - "@smithy/middleware-stack": "^3.0.8", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/protocol-http": "^4.1.5", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.25", - "@smithy/util-defaults-mode-node": "^3.0.25", - "@smithy/util-endpoints": "^2.1.4", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-retry": "^3.0.8", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.687.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.687.0.tgz", - "integrity": "sha512-SQjDH8O4XCTtouuCVYggB0cCCrIaTzUZIkgJUpOsIEJBLlTbNOb/BZqUShAQw2o9vxr2rCeOGjAQOYPysW/Pmg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.687.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/credential-provider-node": "3.687.0", - "@aws-sdk/middleware-host-header": "3.686.0", - "@aws-sdk/middleware-logger": "3.686.0", - "@aws-sdk/middleware-recursion-detection": "3.686.0", - "@aws-sdk/middleware-user-agent": "3.687.0", - "@aws-sdk/region-config-resolver": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-endpoints": "3.686.0", - "@aws-sdk/util-user-agent-browser": "3.686.0", - "@aws-sdk/util-user-agent-node": "3.687.0", - "@smithy/config-resolver": "^3.0.10", - "@smithy/core": "^2.5.1", - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/hash-node": "^3.0.8", - "@smithy/invalid-dependency": "^3.0.8", - "@smithy/middleware-content-length": "^3.0.10", - "@smithy/middleware-endpoint": "^3.2.1", - "@smithy/middleware-retry": "^3.0.25", - "@smithy/middleware-serde": "^3.0.8", - "@smithy/middleware-stack": "^3.0.8", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/protocol-http": "^4.1.5", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.25", - "@smithy/util-defaults-mode-node": "^3.0.25", - "@smithy/util-endpoints": "^2.1.4", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-retry": "^3.0.8", - "@smithy/util-utf8": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", - "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/core": "^2.5.1", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.5", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/util-middleware": "^3.0.8", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.686.0.tgz", - "integrity": "sha512-osD7lPO8OREkgxPiTWmA1i6XEmOth1uW9HWWj/+A2YGCj1G/t2sHu931w4Qj9NWHYZtbTTXQYVRg+TErALV7nQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.686.0.tgz", - "integrity": "sha512-xyGAD/f3vR/wssUiZrNFWQWXZvI4zRm2wpHhoHA1cC2fbRMNFYtFn365yw6dU7l00ZLcdFB1H119AYIUZS7xbw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.5", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/util-stream": "^3.2.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.687.0.tgz", - "integrity": "sha512-6d5ZJeZch+ZosJccksN0PuXv7OSnYEmanGCnbhUqmUSz9uaVX6knZZfHCZJRgNcfSqg9QC0zsFA/51W5HCUqSQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/credential-provider-env": "3.686.0", - "@aws-sdk/credential-provider-http": "3.686.0", - "@aws-sdk/credential-provider-process": "3.686.0", - "@aws-sdk/credential-provider-sso": "3.687.0", - "@aws-sdk/credential-provider-web-identity": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.687.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.687.0.tgz", - "integrity": "sha512-Pqld8Nx11NYaBUrVk3bYiGGpLCxkz8iTONlpQWoVWFhSOzlO7zloNOaYbD2XgFjjqhjlKzE91drs/f41uGeCTA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.686.0", - "@aws-sdk/credential-provider-http": "3.686.0", - "@aws-sdk/credential-provider-ini": "3.687.0", - "@aws-sdk/credential-provider-process": "3.686.0", - "@aws-sdk/credential-provider-sso": "3.687.0", - "@aws-sdk/credential-provider-web-identity": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.686.0.tgz", - "integrity": "sha512-sXqaAgyzMOc+dm4CnzAR5Q6S9OWVHyZjLfW6IQkmGjqeQXmZl24c4E82+w64C+CTkJrFLzH1VNOYp1Hy5gE6Qw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.687.0.tgz", - "integrity": "sha512-N1YCoE7DovIRF2ReyRrA4PZzF0WNi4ObPwdQQkVxhvSm7PwjbWxrfq7rpYB+6YB1Uq3QPzgVwUFONE36rdpxUQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.687.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/token-providers": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.686.0.tgz", - "integrity": "sha512-40UqCpPxyHCXDP7CGd9JIOZDgDZf+u1OyLaGBpjQJlz1HYuEsIWnnbTe29Yg3Ah/Zc3g4NBWcUdlGVotlnpnDg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sts": "^3.686.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.686.0.tgz", - "integrity": "sha512-+Yc6rO02z+yhFbHmRZGvEw1vmzf/ifS9a4aBjJGeVVU+ZxaUvnk+IUZWrj4YQopUQ+bSujmMUzJLXSkbDq7yuw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.686.0.tgz", - "integrity": "sha512-cX43ODfA2+SPdX7VRxu6gXk4t4bdVJ9pkktbfnkE5t27OlwNfvSGGhnHrQL8xTOFeyQ+3T+oowf26gf1OI+vIg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.686.0.tgz", - "integrity": "sha512-jF9hQ162xLgp9zZ/3w5RUNhmwVnXDBlABEUX8jCgzaFpaa742qR/KKtjjZQ6jMbQnP+8fOCSXFAVNMU+s6v81w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.687.0.tgz", - "integrity": "sha512-nUgsKiEinyA50CaDXojAkOasAU3Apdg7Qox6IjNUC4ZjgOu7QWsCDB5N28AYMUt06cNYeYQdfMX1aEzG85a1Mg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-endpoints": "3.686.0", - "@smithy/core": "^2.5.1", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.686.0.tgz", - "integrity": "sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/types": "^3.6.0", - "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.686.0.tgz", - "integrity": "sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.686.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.686.0.tgz", - "integrity": "sha512-7msZE2oYl+6QYeeRBjlDgxQUhq/XRky3cXE0FqLFs2muLS7XSuQEXkpOXB3R782ygAP6JX0kmBxPTLurRTikZg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/types": "^3.6.0", - "@smithy/util-endpoints": "^2.1.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.686.0.tgz", - "integrity": "sha512-YiQXeGYZegF1b7B2GOR61orhgv79qmI0z7+Agm3NXLO6hGfVV3kFUJbXnjtH1BgWo5hbZYW7HQ2omGb3dnb6Lg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/types": "^3.6.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.687.0.tgz", - "integrity": "sha512-idkP6ojSTZ4ek1pJ8wIN7r9U3KR5dn0IkJn3KQBXQ58LWjkRqLtft2vxzdsktWwhPKjjmIKl1S0kbvqLawf8XQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.687.0", - "@aws-sdk/types": "3.686.0", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-s3/node_modules/@smithy/fetch-http-handler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", - "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^4.1.5", - "@smithy/querystring-builder": "^3.0.8", - "@smithy/types": "^3.6.0", - "@smithy/util-base64": "^3.0.0", - "tslib": "^2.6.2" - } - }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.675.0.tgz", - "integrity": "sha512-qC9e56BzlAbKOtvAfbRuGCNDkGjFLi856SeYQ1U9kpegd6+yrFNScKUCJHEZ/clX1zfGPaJCpbEwCtiEaayADw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.693.0.tgz", + "integrity": "sha512-PiXkl64LYhwZQ2zPQhxwpnLwGS7Lw8asFCj29SxEaYRnYra3ajE5d+Yvv68qC+diUNkeZh6k6zn7nEOZ4rWEwA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.675.0", - "@aws-sdk/client-sts": "3.675.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.675.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", @@ -1493,52 +524,52 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.675.0.tgz", - "integrity": "sha512-4/OyFFpHMIahDc063vk4viETLtNPjopcUpwmWMtV8rhOns8KjJ2b1tvpvV7lNYT53mUm+g3fhYok9McHFDeeMA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.693.0.tgz", + "integrity": "sha512-5/bq6eqM/TEoMxzw2R1iD9cuOp5IABsc4wnabN6QwuOqXM0NVC0VopwWh+QKkUU+uzoQV4imLK8AlJ3rLSThfg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.675.0", - "@aws-sdk/client-sts": "3.675.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.675.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/client-sts": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.6", + "@smithy/util-waiter": "^3.1.8", "tslib": "^2.6.2" }, "engines": { @@ -1546,47 +577,47 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.675.0.tgz", - "integrity": "sha512-2goBCEr4acZJ1YJ69eWPTsIfZUbO7enog+lBA5kZShDiwovqzwYSHSlf6OGz4ETs2xT1n7n+QfKY0p+TluTfEw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", + "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -1595,48 +626,48 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.675.0.tgz", - "integrity": "sha512-4kEcaa2P/BFz+xy5tagbtzM08gbjHXyYqW+n6SJuUFK7N6bZNnA4cu1hVgHcqOqk8Dbwv7fiseGT0x3Hhqjwqg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz", + "integrity": "sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.675.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -1644,53 +675,53 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.675.0" + "@aws-sdk/client-sts": "^3.693.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.675.0.tgz", - "integrity": "sha512-zgjyR4GyuONeDGJBKNt9lFJ8HfDX7rpxZZVR7LSXr9lUkjf6vUGgD2k/K4UAoOTWCKKCor6TA562ezGlA8su6Q==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz", + "integrity": "sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.675.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.675.0", - "@aws-sdk/middleware-host-header": "3.667.0", - "@aws-sdk/middleware-logger": "3.667.0", - "@aws-sdk/middleware-recursion-detection": "3.667.0", - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/region-config-resolver": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.675.0", - "@aws-sdk/util-user-agent-node": "3.669.0", - "@smithy/config-resolver": "^3.0.9", - "@smithy/core": "^2.4.8", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/hash-node": "^3.0.7", - "@smithy/invalid-dependency": "^3.0.7", - "@smithy/middleware-content-length": "^3.0.9", - "@smithy/middleware-endpoint": "^3.1.4", - "@smithy/middleware-retry": "^3.0.23", - "@smithy/middleware-serde": "^3.0.7", - "@smithy/middleware-stack": "^3.0.7", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/url-parser": "^3.0.7", + "@aws-sdk/client-sso-oidc": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-node": "3.693.0", + "@aws-sdk/middleware-host-header": "3.693.0", + "@aws-sdk/middleware-logger": "3.693.0", + "@aws-sdk/middleware-recursion-detection": "3.693.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/region-config-resolver": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@aws-sdk/util-user-agent-browser": "3.693.0", + "@aws-sdk/util-user-agent-node": "3.693.0", + "@smithy/config-resolver": "^3.0.11", + "@smithy/core": "^2.5.2", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/hash-node": "^3.0.9", + "@smithy/invalid-dependency": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.11", + "@smithy/middleware-endpoint": "^3.2.2", + "@smithy/middleware-retry": "^3.0.26", + "@smithy/middleware-serde": "^3.0.9", + "@smithy/middleware-stack": "^3.0.9", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/url-parser": "^3.0.9", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.23", - "@smithy/util-defaults-mode-node": "^3.0.23", - "@smithy/util-endpoints": "^2.1.3", - "@smithy/util-middleware": "^3.0.7", - "@smithy/util-retry": "^3.0.7", + "@smithy/util-defaults-mode-browser": "^3.0.26", + "@smithy/util-defaults-mode-node": "^3.0.26", + "@smithy/util-endpoints": "^2.1.5", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-retry": "^3.0.9", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -1699,20 +730,20 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.667.0.tgz", - "integrity": "sha512-pMcDVI7Tmdsc8R3sDv0Omj/4iRParGY+uJtAfF669WnZfDfaBQaix2Mq7+Mu08vdjqO9K3gicFvjk9S1VLmOKA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", + "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/core": "^2.4.8", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-middleware": "^3.0.7", + "@aws-sdk/types": "3.692.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, @@ -1721,15 +752,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.667.0.tgz", - "integrity": "sha512-zZbrkkaPc54WXm+QAnpuv0LPNfsts0HPPd+oCECGs7IQRaFsGj187cwvPg9RMWDFZqpm64MdBDoA8OQHsqzYCw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.693.0.tgz", + "integrity": "sha512-hMUZaRSF7+iBKZfBHNLihFs9zvpM1CB8MBOTnTp5NGCVkRYF3SB2LH+Kcippe0ats4qCyB1eEoyQX99rERp2iQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1737,20 +768,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.667.0.tgz", - "integrity": "sha512-sjtybFfERZWiqTY7fswBxKQLvUkiCucOWyqh3IaPo/4nE1PXRnaZCVG0+kRBPrYIxWqiVwytvZzMJy8sVZcG0A==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", + "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/fetch-http-handler": "^3.2.9", - "@smithy/node-http-handler": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.4", - "@smithy/smithy-client": "^3.4.0", - "@smithy/types": "^3.5.0", - "@smithy/util-stream": "^3.1.9", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/fetch-http-handler": "^4.1.0", + "@smithy/node-http-handler": "^3.3.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/protocol-http": "^4.1.6", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", + "@smithy/util-stream": "^3.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1758,48 +789,48 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.675.0.tgz", - "integrity": "sha512-kCBlC6grpbpCvgowk9T4JHZxJ88VfN0r77bDZClcadFRAKQ8UHyO02zhgFCfUdnU1lNv1mr3ngEcGN7XzJlYWA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", + "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-env": "3.667.0", - "@aws-sdk/credential-provider-http": "3.667.0", - "@aws-sdk/credential-provider-process": "3.667.0", - "@aws-sdk/credential-provider-sso": "3.675.0", - "@aws-sdk/credential-provider-web-identity": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.675.0" + "@aws-sdk/client-sts": "^3.693.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.675.0.tgz", - "integrity": "sha512-VO1WVZCDmAYu4sY/6qIBzdm5vJTxLhWKJWvL5kVFfSe8WiNNoHlTqYYUK9vAm/JYpIgFLTefPbIc5W4MK7o6Pg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", + "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.667.0", - "@aws-sdk/credential-provider-http": "3.667.0", - "@aws-sdk/credential-provider-ini": "3.675.0", - "@aws-sdk/credential-provider-process": "3.667.0", - "@aws-sdk/credential-provider-sso": "3.675.0", - "@aws-sdk/credential-provider-web-identity": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/credential-provider-imds": "^3.2.4", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/credential-provider-env": "3.693.0", + "@aws-sdk/credential-provider-http": "3.693.0", + "@aws-sdk/credential-provider-ini": "3.693.0", + "@aws-sdk/credential-provider-process": "3.693.0", + "@aws-sdk/credential-provider-sso": "3.693.0", + "@aws-sdk/credential-provider-web-identity": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/credential-provider-imds": "^3.2.6", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1807,16 +838,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.667.0.tgz", - "integrity": "sha512-HZHnvop32fKgsNHkdhVaul7UzQ25sEc0j9yqA4bjhtbk0ECl42kj3f1pJ+ZU/YD9ut8lMJs/vVqiOdNThVdeBw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.693.0.tgz", + "integrity": "sha512-cvxQkrTWHHjeHrPlj7EWXPnFSq8x7vMx+Zn1oTsMpCY445N9KuzjfJTkmNGwU2GT6rSZI9/0MM02aQvl5bBBTQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1824,18 +855,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.675.0.tgz", - "integrity": "sha512-p/EE2c0ebSgRhg1Fe1OH2+xNl7j1P4DTc7kZy1mX1NJ72fkqnGgBuf1vk5J9RmiRpbauPNMlm+xohjkGS7iodA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", + "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.675.0", - "@aws-sdk/core": "3.667.0", - "@aws-sdk/token-providers": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/client-sso": "3.693.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/token-providers": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1843,35 +874,35 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.667.0.tgz", - "integrity": "sha512-t8CFlZMD/1p/8Cli3rvRiTJpjr/8BO64gw166AHgFZYSN2h95L2l1tcW0jpsc3PprA32nLg1iQVKYt4WGM4ugw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", + "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.667.0" + "@aws-sdk/client-sts": "^3.693.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.686.0.tgz", - "integrity": "sha512-6qCoWI73/HDzQE745MHQUYz46cAQxHCgy1You8MZQX9vHAQwqBnkcsb2hGp7S6fnQY5bNsiZkMWVQ/LVd2MNjg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.693.0.tgz", + "integrity": "sha512-cPIa+lxMYiFRHtxKfNIVSFGO6LSgZCk42pu3d7KGwD6hu6vXRD5B2/DD3rPcEH1zgl2j0Kx1oGAV7SRXKHSFag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-arn-parser": "3.679.0", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-arn-parser": "3.693.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "@smithy/util-config-provider": "^3.0.0", "tslib": "^2.6.2" }, @@ -1879,41 +910,15 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.686.0.tgz", - "integrity": "sha512-5yYqIbyhLhH29vn4sHiTj7sU6GttvLMk3XwCmBXjo2k2j3zHqFUwh9RyFGF9VY6Z392Drf/E/cl+qOGypwULpg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.693.0.tgz", + "integrity": "sha512-MuK/gsJWpHz6Tv0CqTCS+QNOxLa2RfPh1biVCu/uO3l7kA0TjQ/C+tfgKvLXeH103tuDrOVINK+bt2ENmI3SWg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1921,22 +926,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.689.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.689.0.tgz", - "integrity": "sha512-6VxMOf3mgmAgg6SMagwKj5pAe+putcx2F2odOAWviLcobFpdM/xK9vNry7p6kY+RDNmSlBvcji9wnU59fjV74Q==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.693.0.tgz", + "integrity": "sha512-xkS6zjuE11ob93H9t65kHzphXcUMnN2SmIm2wycUPg+hi8Q6DJA6U2p//6oXkrr9oHy1QvwtllRd7SAd63sFKQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", "@smithy/is-array-buffer": "^3.0.0", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-stream": "^3.2.1", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-stream": "^3.3.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -1944,50 +949,15 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", - "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/core": "^2.5.1", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.5", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/util-middleware": "^3.0.8", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.667.0.tgz", - "integrity": "sha512-Z7fIAMQnPegs7JjAQvlOeWXwpMRfegh5eCoIP6VLJIeR6DLfYKbP35JBtt98R6DXslrN2RsbTogjbxPEDQfw1w==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", + "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -1995,26 +965,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.686.0.tgz", - "integrity": "sha512-pCLeZzt5zUGY3NbW4J/5x3kaHyJEji4yqtoQcUlJmkoEInhSxJ0OE8sTxAfyL3nIOF4yr6L2xdaLCqYgQT8Aog==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.693.0.tgz", + "integrity": "sha512-eDAExTZ9uNIP7vs2JCVCOuWJauGueisBSn+Ovt7UvvuEUp6KOIJqn8oFxWmyUQu2GvbG4OcaTLgbqD95YHTB0Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2022,13 +979,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.667.0.tgz", - "integrity": "sha512-PtTRNpNm/5c746jRgZCNg4X9xEJIwggkGJrF0GP9AB1ANg4pc/sF2Fvn1NtqPe9wtQ2stunJprnm5WkCHN7QiA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", + "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2036,14 +993,14 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.667.0.tgz", - "integrity": "sha512-U5glWD3ehFohzpUpopLtmqAlDurGWo2wRGPNgi4SwhWU7UDt6LS7E/UvJjqC0CUrjlzOw+my2A+Ncf+fisMhxQ==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", + "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2051,23 +1008,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.687.0.tgz", - "integrity": "sha512-YGHYqiyRiNNucmvLrfx3QxIkjSDWR/+cc72bn0lPvqFUQBRHZgmYQLxVYrVZSmRzzkH2FQ1HsZcXhOafLbq4vQ==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.693.0.tgz", + "integrity": "sha512-5A++RBjJ3guyq5pbYs+Oq5hMlA8CK2OWaHx09cxVfhHWl/RoaY8DXrft4gnhoUEBrrubyMw7r9j7RIMLvS58kg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.686.0", - "@aws-sdk/types": "3.686.0", - "@aws-sdk/util-arn-parser": "3.679.0", - "@smithy/core": "^2.5.1", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.5", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-arn-parser": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/smithy-client": "^3.4.3", + "@smithy/types": "^3.7.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-stream": "^3.2.1", + "@smithy/util-middleware": "^3.0.9", + "@smithy/util-stream": "^3.3.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -2075,62 +1032,14 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/core": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.686.0.tgz", - "integrity": "sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/core": "^2.5.1", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/property-provider": "^3.1.7", - "@smithy/protocol-http": "^4.1.5", - "@smithy/signature-v4": "^4.2.0", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/util-middleware": "^3.0.8", - "fast-xml-parser": "4.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.686.0.tgz", - "integrity": "sha512-zJXml/CpVHFUdlGQqja87vNQ3rPB5SlDbfdwxlj1KBbjnRRwpBtxxmOlWRShg8lnVV6aIMGv95QmpIFy4ayqnQ==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.693.0.tgz", + "integrity": "sha512-Ro5vzI7SRgEeuoMk3fKqFjGv6mG4c7VsSCDwnkiasmafQFBTPvUIpgmu2FXMHqW/OthvoiOzpSrlJ9Bwlx2f8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.686.0", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2138,17 +1047,17 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.669.0.tgz", - "integrity": "sha512-K8ScPi45zjJrj5Y2gRqVsvKKQCQbvQBfYGcBw9ZOx9TTavH80bOCBjWg/GFnvs4f37tqVc1wMN2oGvcTF6HveQ==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", + "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.667.0", - "@aws-sdk/types": "3.667.0", - "@aws-sdk/util-endpoints": "3.667.0", - "@smithy/core": "^2.4.8", - "@smithy/protocol-http": "^4.1.4", - "@smithy/types": "^3.5.0", + "@aws-sdk/core": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@aws-sdk/util-endpoints": "3.693.0", + "@smithy/core": "^2.5.2", + "@smithy/protocol-http": "^4.1.6", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2156,16 +1065,16 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.667.0.tgz", - "integrity": "sha512-iNr+JhhA902JMKHG9IwT9YdaEx6KGl6vjAL5BRNeOjfj4cZYMog6Lz/IlfOAltMtT0w88DAHDEFrBd2uO0l2eg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", + "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.7", + "@smithy/util-middleware": "^3.0.9", "tslib": "^2.6.2" }, "engines": { @@ -2173,29 +1082,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.687.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.687.0.tgz", - "integrity": "sha512-vdOQHCRHJPX9mT8BM6xOseazHD6NodvHl9cyF5UjNtLn+gERRJEItIA9hf0hlt62odGD8Fqp+rFRuqdmbNkcNw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.693.0.tgz", + "integrity": "sha512-s7zbbsoVIriTR4ZGaateKuTqz6ddpazAyHvjk7I9kd+NvGNPiuAI18UdbuiiRI6K5HuYKf1ah6mKWFGPG15/kQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.687.0", - "@aws-sdk/types": "3.686.0", - "@smithy/protocol-http": "^4.1.5", - "@smithy/signature-v4": "^4.2.0", - "@smithy/types": "^3.6.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/types": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.686.0.tgz", - "integrity": "sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^3.6.0", + "@aws-sdk/middleware-sdk-s3": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/protocol-http": "^4.1.6", + "@smithy/signature-v4": "^4.2.2", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2203,31 +1099,31 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.667.0.tgz", - "integrity": "sha512-ZecJlG8p6D4UTYlBHwOWX6nknVtw/OBJ3yPXTSajBjhUlj9lE2xvejI8gl4rqkyLXk7z3bki+KR4tATbMaM9yg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", + "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/property-provider": "^3.1.7", - "@smithy/shared-ini-file-loader": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/property-provider": "^3.1.9", + "@smithy/shared-ini-file-loader": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sso-oidc": "^3.667.0" + "@aws-sdk/client-sso-oidc": "^3.693.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.667.0.tgz", - "integrity": "sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==", + "version": "3.692.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.692.0.tgz", + "integrity": "sha512-RpNvzD7zMEhiKgmlxGzyXaEcg2khvM7wd5sSHVapOcrde1awQSOMGI4zKBQ+wy5TnDfrm170ROz/ERLYtrjPZA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2235,9 +1131,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.679.0.tgz", - "integrity": "sha512-CwzEbU8R8rq9bqUFryO50RFBlkfufV9UfMArHPWlo+lmsC+NlSluHQALoj6Jkq3zf5ppn1CN0c1DDLrEqdQUXg==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.693.0.tgz", + "integrity": "sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2247,14 +1143,14 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.667.0.tgz", - "integrity": "sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", + "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/types": "^3.5.0", - "@smithy/util-endpoints": "^2.1.3", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", + "@smithy/util-endpoints": "^2.1.5", "tslib": "^2.6.2" }, "engines": { @@ -2273,27 +1169,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.675.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.675.0.tgz", - "integrity": "sha512-HW4vGfRiX54RLcsYjLuAhcBBJ6lRVEZd7njfGpAwBB9s7BH8t48vrpYbyA5XbbqbTvXfYBnugQCUw9HWjEa1ww==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", + "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.667.0", - "@smithy/types": "^3.5.0", + "@aws-sdk/types": "3.692.0", + "@smithy/types": "^3.7.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.669.0.tgz", - "integrity": "sha512-9jxCYrgggy2xd44ZASqI7AMiRVaSiFp+06Kg8BQSU0ijKpBJlwcsqIS8pDT/n6LxuOw2eV5ipvM2C0r1iKzrGA==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", + "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.669.0", - "@aws-sdk/types": "3.667.0", - "@smithy/node-config-provider": "^3.1.8", - "@smithy/types": "^3.5.0", + "@aws-sdk/middleware-user-agent": "3.693.0", + "@aws-sdk/types": "3.692.0", + "@smithy/node-config-provider": "^3.1.10", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2309,12 +1205,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.686.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.686.0.tgz", - "integrity": "sha512-k0z5b5dkYSuOHY0AOZ4iyjcGBeVL9lWsQNF4+c+1oK3OW4fRWl/bNa1soMRMpangsHPzgyn/QkzuDbl7qR4qrw==", + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.693.0.tgz", + "integrity": "sha512-C/rPwJcqnV8VDr2/VtcQnymSpcfEEgH1Jm6V0VmfXNZFv4Qzf1eCS8nsec0gipYgZB+cBBjfXw5dAk6pJ8ubpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.0", "tslib": "^2.6.2" }, "engines": { @@ -2742,87 +1638,113 @@ "license": "MIT" }, "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", - "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" }, "node_modules/@firebase/app-types": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", - "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" }, "node_modules/@firebase/auth-interop-types": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", - "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" }, "node_modules/@firebase/component": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", - "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz", + "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==", + "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.9.3", + "@firebase/util": "1.10.2", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@firebase/database": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.2.tgz", - "integrity": "sha512-8X6NBJgUQzDz0xQVaCISoOLINKat594N2eBbMR3Mu/MH/ei4WM+aAMlsNzngF22eljXu1SILP5G3evkyvsG3Ng==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz", + "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==", + "license": "Apache-2.0", "dependencies": { - "@firebase/app-check-interop-types": "0.3.0", - "@firebase/auth-interop-types": "0.2.1", - "@firebase/component": "0.6.4", - "@firebase/logger": "0.4.0", - "@firebase/util": "1.9.3", + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.11", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", "faye-websocket": "0.11.4", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@firebase/database-compat": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.2.tgz", - "integrity": "sha512-09ryJnXDvuycsxn8aXBzLhBTuCos3HEnCOBWY6hosxfYlNCGnLvG8YMlbSAt5eNhf7/00B095AEfDsdrrLjxqA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz", + "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==", + "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.4", - "@firebase/database": "1.0.2", - "@firebase/database-types": "1.0.0", - "@firebase/logger": "0.4.0", - "@firebase/util": "1.9.3", + "@firebase/component": "0.6.11", + "@firebase/database": "1.0.10", + "@firebase/database-types": "1.0.7", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@firebase/database-types": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", - "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz", + "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==", + "license": "Apache-2.0", "dependencies": { - "@firebase/app-types": "0.9.0", - "@firebase/util": "1.9.3" + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.10.2" } }, "node_modules/@firebase/logger": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", - "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@firebase/util": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", - "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz", + "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@google-cloud/firestore": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.7.0.tgz", - "integrity": "sha512-41/vBFXOeSYjFI/2mJuJrDwg2umGk+FDrI/SCGzBRUe+UZWDN4GoahIbGZ19YQsY0ANNl6DRiAy4wD6JezK02g==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.10.0.tgz", + "integrity": "sha512-VFNhdHvfnmqcHHs6YhmSNHHxQqaaD64GwiL0c+e1qz85S8SWZPC2XFRf8p9yHRTF40Kow424s1KBU9f0fdQa+Q==", + "license": "Apache-2.0", "optional": true, "dependencies": { + "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", @@ -2833,9 +1755,10 @@ } }, "node_modules/@google-cloud/paginator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", - "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", "optional": true, "dependencies": { "arrify": "^2.0.0", @@ -2849,6 +1772,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", "optional": true, "engines": { "node": ">=14.0.0" @@ -2858,15 +1782,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@google-cloud/storage": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.7.0.tgz", - "integrity": "sha512-EMCEY+6JiIkx7Dt8NXVGGjy1vRdSGdHkoqZoqjJw7cEBkT7ZkX0c7puedfn1MamnzW5SX4xoa2jVq5u7OWBmkQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.14.0.tgz", + "integrity": "sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", @@ -2874,14 +1800,12 @@ "@google-cloud/promisify": "^4.0.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", - "compressible": "^2.0.12", - "duplexify": "^4.0.0", - "ent": "^2.2.0", - "fast-xml-parser": "^4.3.0", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", - "google-auth-library": "^9.0.0", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", "mime": "^3.0.0", - "mime-types": "^2.0.8", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", @@ -2895,6 +1819,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -2909,9 +1834,10 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz", - "integrity": "sha512-vYVqYzHicDqyKB+NQhAc54I1QWCBLCrYG6unqOIcBTHx+7x8C9lcoLj3KVJXs2VB4lUbpWY+Kk9NipcbXYWmvg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", + "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "license": "Apache-2.0", "optional": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -2925,6 +1851,7 @@ "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==", + "license": "Apache-2.0", "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", @@ -3091,6 +2018,7 @@ "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==", + "license": "MIT", "optional": true, "funding": { "type": "opencollective", @@ -3221,15 +2149,15 @@ } }, "node_modules/@opensearch-project/opensearch": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.12.0.tgz", - "integrity": "sha512-FNGWbWjvpWIZHVvAbv0FkSgvc1PnWnYEHnOTeIY08vMDp9QpXumGNDjNc1tZthJ3OEeoooqH0miGFORjWnRYsQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz", + "integrity": "sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==", "license": "Apache-2.0", "dependencies": { "aws4": "^1.11.0", "debug": "^4.3.1", "hpagent": "^1.2.0", - "json11": "^1.1.2", + "json11": "^2.0.0", "ms": "^2.1.3", "secure-json-parse": "^2.4.0" }, @@ -3238,6 +2166,16 @@ "yarn": "^1.22.10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3251,30 +2189,35 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -3285,30 +2228,35 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true }, "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==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@redis/bloom": { @@ -3371,12 +2319,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz", - "integrity": "sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.8.tgz", + "integrity": "sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3403,15 +2351,15 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz", - "integrity": "sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.12.tgz", + "integrity": "sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.9", - "@smithy/types": "^3.6.0", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.1", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.8", + "@smithy/util-middleware": "^3.0.10", "tslib": "^2.6.2" }, "engines": { @@ -3419,17 +2367,17 @@ } }, "node_modules/@smithy/core": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz", - "integrity": "sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.3.tgz", + "integrity": "sha512-96uW8maifUSmehaeW7uydWn7wBc98NEeNI3zN8vqakGpyCQgzyJaA64Z4FCOUmAdCJkhppd/7SZ798Fo4Xx37g==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^3.0.8", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", + "@smithy/middleware-serde": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-stream": "^3.2.1", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-stream": "^3.3.1", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -3438,15 +2386,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz", - "integrity": "sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.7.tgz", + "integrity": "sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.9", - "@smithy/property-provider": "^3.1.8", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", "tslib": "^2.6.2" }, "engines": { @@ -3454,25 +2402,25 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.7.tgz", - "integrity": "sha512-kVSXScIiRN7q+s1x7BrQtZ1Aa9hvvP9FeCqCdBxv37GimIHgBCOnZ5Ip80HLt0DhnAKpiobFdGqTFgbaJNrazA==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.9.tgz", + "integrity": "sha512-F574nX0hhlNOjBnP+noLtsPFqXnWh2L0+nZKCwcu7P7J8k+k+rdIDs+RMnrMwrzhUE4mwMgyN0cYnEn0G8yrnQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "@smithy/util-hex-encoding": "^3.0.0", "tslib": "^2.6.2" } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.11.tgz", - "integrity": "sha512-Pd1Wnq3CQ/v2SxRifDUihvpXzirJYbbtXfEnnLV/z0OGCTx/btVX74P86IgrZkjOydOASBGXdPpupYQI+iO/6A==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.13.tgz", + "integrity": "sha512-Nee9m+97o9Qj6/XeLz2g2vANS2SZgAxV4rDBMKGHvFJHU/xz88x2RwCkwsvEwYjSX4BV1NG1JXmxEaDUzZTAtw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^3.0.10", - "@smithy/types": "^3.6.0", + "@smithy/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3480,12 +2428,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.8.tgz", - "integrity": "sha512-zkFIG2i1BLbfoGQnf1qEeMqX0h5qAznzaZmMVNnvPZz9J5AWBPkOMckZWPedGUPcVITacwIdQXoPcdIQq5FRcg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.10.tgz", + "integrity": "sha512-K1M0x7P7qbBUKB0UWIL5KOcyi6zqV5mPJoL0/o01HPJr0CSq3A9FYuJC6e11EX6hR8QTIR++DBiGrYveOu6trw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3493,13 +2441,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.10.tgz", - "integrity": "sha512-hjpU1tIsJ9qpcoZq9zGHBJPBOeBGYt+n8vfhDwnITPhEre6APrvqq/y3XMDEGUT2cWQ4ramNqBPRbx3qn55rhw==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.12.tgz", + "integrity": "sha512-kiZymxXvZ4tnuYsPSMUHe+MMfc4FTeFWJIc0Q5wygJoUQM4rVHNghvd48y7ppuulNMbuYt95ah71pYc2+o4JOA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^3.0.10", - "@smithy/types": "^3.6.0", + "@smithy/eventstream-serde-universal": "^3.0.12", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3507,13 +2455,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.10.tgz", - "integrity": "sha512-ewG1GHbbqsFZ4asaq40KmxCmXO+AFSM1b+DcO2C03dyJj/ZH71CiTg853FSE/3SHK9q3jiYQIFjlGSwfxQ9kww==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.12.tgz", + "integrity": "sha512-1i8ifhLJrOZ+pEifTlF0EfZzMLUGQggYQ6WmZ4d5g77zEKf7oZ0kvh1yKWHPjofvOwqrkwRDVuxuYC8wVd662A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^3.1.7", - "@smithy/types": "^3.6.0", + "@smithy/eventstream-codec": "^3.1.9", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3521,37 +2469,37 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", - "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.1.tgz", + "integrity": "sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.4", - "@smithy/querystring-builder": "^3.0.7", - "@smithy/types": "^3.5.0", + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" } }, "node_modules/@smithy/hash-blob-browser": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.7.tgz", - "integrity": "sha512-4yNlxVNJifPM5ThaA5HKnHkn7JhctFUHvcaz6YXxHlYOSIrzI6VKQPTN8Gs1iN5nqq9iFcwIR9THqchUCouIfg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.9.tgz", + "integrity": "sha512-wOu78omaUuW5DE+PVWXiRKWRZLecARyP3xcq5SmkXUw9+utgN8HnSnBfrjL2B/4ZxgqPjaAJQkC/+JHf1ITVaQ==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^4.0.0", "@smithy/chunked-blob-reader-native": "^3.0.1", - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" } }, "node_modules/@smithy/hash-node": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz", - "integrity": "sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.10.tgz", + "integrity": "sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -3561,12 +2509,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.7.tgz", - "integrity": "sha512-xMAsvJ3hLG63lsBVi1Hl6BBSfhd8/Qnp8fC06kjOpJvyyCEXdwHITa5Kvdsk6gaAXLhbZMhQMIGvgUbfnJDP6Q==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.9.tgz", + "integrity": "sha512-3XfHBjSP3oDWxLmlxnt+F+FqXpL3WlXs+XXaB6bV9Wo8BBu87fK1dSEsyH7Z4ZHRmwZ4g9lFMdf08m9hoX1iRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -3575,12 +2523,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz", - "integrity": "sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.10.tgz", + "integrity": "sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" } }, @@ -3596,24 +2544,24 @@ } }, "node_modules/@smithy/md5-js": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.8.tgz", - "integrity": "sha512-LwApfTK0OJ/tCyNUXqnWCKoE2b4rDSr4BJlDAVCkiWYeHESr+y+d5zlAanuLW6fnitVJRD/7d9/kN/ZM9Su4mA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.10.tgz", + "integrity": "sha512-m3bv6dApflt3fS2Y1PyWPUtRP7iuBlvikEOGwu0HsCZ0vE7zcIX+dBoh3e+31/rddagw8nj92j0kJg2TfV+SJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "node_modules/@smithy/middleware-content-length": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz", - "integrity": "sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.12.tgz", + "integrity": "sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3621,18 +2569,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz", - "integrity": "sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.3.tgz", + "integrity": "sha512-Hdl9296i/EMptaX7agrSzJZDiz5Y8XPUeBbctTmMtnCguGpqfU3jVsTUan0VLaOhsnquqWLL8Bl5HrlbVGT1og==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^2.5.1", - "@smithy/middleware-serde": "^3.0.8", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/shared-ini-file-loader": "^3.1.9", - "@smithy/types": "^3.6.0", - "@smithy/url-parser": "^3.0.8", - "@smithy/util-middleware": "^3.0.8", + "@smithy/core": "^2.5.3", + "@smithy/middleware-serde": "^3.0.10", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", + "@smithy/url-parser": "^3.0.10", + "@smithy/util-middleware": "^3.0.10", "tslib": "^2.6.2" }, "engines": { @@ -3640,18 +2588,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz", - "integrity": "sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg==", + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.27.tgz", + "integrity": "sha512-H3J/PjJpLL7Tt+fxDKiOD25sMc94YetlQhCnYeNmina2LZscAdu0ZEZPas/kwePHABaEtqp7hqa5S4UJgMs1Tg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.9", - "@smithy/protocol-http": "^4.1.5", - "@smithy/service-error-classification": "^3.0.8", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", - "@smithy/util-middleware": "^3.0.8", - "@smithy/util-retry": "^3.0.8", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/protocol-http": "^4.1.7", + "@smithy/service-error-classification": "^3.0.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", + "@smithy/util-middleware": "^3.0.10", + "@smithy/util-retry": "^3.0.10", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -3673,12 +2621,12 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz", - "integrity": "sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.10.tgz", + "integrity": "sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3686,12 +2634,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz", - "integrity": "sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.10.tgz", + "integrity": "sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3699,14 +2647,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz", - "integrity": "sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.11.tgz", + "integrity": "sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.8", - "@smithy/shared-ini-file-loader": "^3.1.9", - "@smithy/types": "^3.6.0", + "@smithy/property-provider": "^3.1.10", + "@smithy/shared-ini-file-loader": "^3.1.11", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3714,15 +2662,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz", - "integrity": "sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.1.tgz", + "integrity": "sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^3.1.6", - "@smithy/protocol-http": "^4.1.5", - "@smithy/querystring-builder": "^3.0.8", - "@smithy/types": "^3.6.0", + "@smithy/abort-controller": "^3.1.8", + "@smithy/protocol-http": "^4.1.7", + "@smithy/querystring-builder": "^3.0.10", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3730,12 +2678,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz", - "integrity": "sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.10.tgz", + "integrity": "sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3743,12 +2691,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz", - "integrity": "sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.7.tgz", + "integrity": "sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3756,12 +2704,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz", - "integrity": "sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz", + "integrity": "sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -3770,12 +2718,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz", - "integrity": "sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.10.tgz", + "integrity": "sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3783,24 +2731,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz", - "integrity": "sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.10.tgz", + "integrity": "sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0" + "@smithy/types": "^3.7.1" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz", - "integrity": "sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.11.tgz", + "integrity": "sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3808,16 +2756,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz", - "integrity": "sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.3.tgz", + "integrity": "sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", "@smithy/util-hex-encoding": "^3.0.0", - "@smithy/util-middleware": "^3.0.8", + "@smithy/util-middleware": "^3.0.10", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -3827,17 +2775,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz", - "integrity": "sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.4.tgz", + "integrity": "sha512-dPGoJuSZqvirBq+yROapBcHHvFjChoAQT8YPWJ820aPHHiowBlB3RL1Q4kPT1hx0qKgJuf+HhyzKi5Gbof4fNA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^2.5.1", - "@smithy/middleware-endpoint": "^3.2.1", - "@smithy/middleware-stack": "^3.0.8", - "@smithy/protocol-http": "^4.1.5", - "@smithy/types": "^3.6.0", - "@smithy/util-stream": "^3.2.1", + "@smithy/core": "^2.5.3", + "@smithy/middleware-endpoint": "^3.2.3", + "@smithy/middleware-stack": "^3.0.10", + "@smithy/protocol-http": "^4.1.7", + "@smithy/types": "^3.7.1", + "@smithy/util-stream": "^3.3.1", "tslib": "^2.6.2" }, "engines": { @@ -3845,9 +2793,9 @@ } }, "node_modules/@smithy/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz", - "integrity": "sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.1.tgz", + "integrity": "sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3857,13 +2805,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz", - "integrity": "sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.10.tgz", + "integrity": "sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^3.0.8", - "@smithy/types": "^3.6.0", + "@smithy/querystring-parser": "^3.0.10", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" } }, @@ -3924,14 +2872,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz", - "integrity": "sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA==", + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.27.tgz", + "integrity": "sha512-GV8NvPy1vAGp7u5iD/xNKUxCorE4nQzlyl057qRac+KwpH5zq8wVq6rE3lPPeuFLyQXofPN6JwxL1N9ojGapiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^3.1.8", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -3940,17 +2888,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.25", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz", - "integrity": "sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g==", + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.27.tgz", + "integrity": "sha512-7+4wjWfZqZxZVJvDutO+i1GvL6bgOajEkop4FuR6wudFlqBiqwxw3HoH6M9NgeCd37km8ga8NPp2JacQEtAMPg==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^3.0.10", - "@smithy/credential-provider-imds": "^3.2.5", - "@smithy/node-config-provider": "^3.1.9", - "@smithy/property-provider": "^3.1.8", - "@smithy/smithy-client": "^3.4.2", - "@smithy/types": "^3.6.0", + "@smithy/config-resolver": "^3.0.12", + "@smithy/credential-provider-imds": "^3.2.7", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/property-provider": "^3.1.10", + "@smithy/smithy-client": "^3.4.4", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3958,13 +2906,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz", - "integrity": "sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.6.tgz", + "integrity": "sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^3.1.9", - "@smithy/types": "^3.6.0", + "@smithy/node-config-provider": "^3.1.11", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3984,12 +2932,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz", - "integrity": "sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.10.tgz", + "integrity": "sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.6.0", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -3997,13 +2945,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz", - "integrity": "sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.10.tgz", + "integrity": "sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^3.0.8", - "@smithy/types": "^3.6.0", + "@smithy/service-error-classification": "^3.0.10", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -4011,14 +2959,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz", - "integrity": "sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.1.tgz", + "integrity": "sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^4.0.0", - "@smithy/node-http-handler": "^3.2.5", - "@smithy/types": "^3.6.0", + "@smithy/fetch-http-handler": "^4.1.1", + "@smithy/node-http-handler": "^3.3.1", + "@smithy/types": "^3.7.1", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -4029,19 +2977,6 @@ "node": ">=16.0.0" } }, - "node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz", - "integrity": "sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^4.1.5", - "@smithy/querystring-builder": "^3.0.8", - "@smithy/types": "^3.6.0", - "@smithy/util-base64": "^3.0.0", - "tslib": "^2.6.2" - } - }, "node_modules/@smithy/util-uri-escape": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", @@ -4067,13 +3002,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.7.tgz", - "integrity": "sha512-d5yGlQtmN/z5eoTtIYgkvOw27US2Ous4VycnXatyoImIF9tzlcpnKqQ/V7qhvJmb2p6xZne1NopCLakdTnkBBQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.9.tgz", + "integrity": "sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^3.1.6", - "@smithy/types": "^3.6.0", + "@smithy/abort-controller": "^3.1.8", + "@smithy/types": "^3.7.1", "tslib": "^2.6.2" }, "engines": { @@ -4166,6 +3101,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", "optional": true }, "node_modules/@types/connect": { @@ -4230,6 +3166,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", "optional": true }, "node_modules/@types/mime": { @@ -4238,12 +3175,12 @@ "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==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.18.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/qs": { @@ -4260,6 +3197,7 @@ "version": "2.48.12", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", "optional": true, "dependencies": { "@types/caseless": "*", @@ -4269,19 +3207,42 @@ } }, "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "license": "MIT", "optional": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" } }, + "node_modules/@types/request/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" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -4302,9 +3263,10 @@ } }, "node_modules/@types/tough-cookie": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.11.tgz", - "integrity": "sha512-xtFyCxnfpItBS6wRt6M+be0PzNEP6J/CqTR0mHCf/OzIbbOOh6DQ1MjiyzDrzDctzgYSmRcHH3PBvTO2hYovLg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", "optional": true }, "node_modules/@types/triple-beam": { @@ -4344,6 +3306,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -4447,6 +3411,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -4485,6 +3450,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", "optional": true, "dependencies": { "retry": "0.13.1" @@ -4556,7 +3522,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/base64id": { "version": "2.0.0", @@ -4694,30 +3661,6 @@ "node": ">= 0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "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", @@ -4816,9 +3759,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", - "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -5002,30 +3945,23 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "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==", - "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", @@ -5039,6 +3975,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/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" + } + ], + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5564,15 +4529,16 @@ "dev": true }, "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", "optional": true, "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" + "stream-shift": "^1.0.2" } }, "node_modules/eastasianwidth": { @@ -5631,6 +4597,7 @@ "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==", + "license": "MIT", "optional": true, "dependencies": { "once": "^1.4.0" @@ -5666,12 +4633,6 @@ "node": ">=10.0.0" } }, - "node_modules/ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", - "optional": true - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5794,19 +4755,12 @@ "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==", + "license": "MIT", + "optional": 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==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/express": { "version": "4.21.1", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", @@ -5894,7 +4848,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true + "license": "MIT" }, "node_modules/extract-css": { "version": "3.0.1", @@ -5919,6 +4873,7 @@ "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==", + "license": "MIT", "optional": true }, "node_modules/fast-levenshtein": { @@ -5957,6 +4912,7 @@ "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -6029,27 +4985,41 @@ "license": "MIT" }, "node_modules/firebase-admin": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.6.0.tgz", - "integrity": "sha512-gc0pDiUmxscxBhcjMcttmjvExJmnQdVRb+IIth95CvMm7F9rLdabrQZThW2mK02HR696P+rzd6NqkdUA3URu4w==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.0.0.tgz", + "integrity": "sha512-tgm4+NT051tv237g4rLz6L5TJ4l1QwPjzysBJKnukP8fvdJQuXUNpqQONptNbNeLEUkRAroGNuEg5v3aVPzkbw==", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^1.0.2", - "@firebase/database-types": "^1.0.0", - "@types/node": "^22.0.1", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", - "uuid": "^10.0.0" + "uuid": "^11.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "optionalDependencies": { - "@google-cloud/firestore": "^7.7.0", - "@google-cloud/storage": "^7.7.0" + "@google-cloud/firestore": "^7.10.0", + "@google-cloud/storage": "^7.14.0" + } + }, + "node_modules/firebase-admin/node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" } }, "node_modules/flat-util": { @@ -6225,6 +5195,7 @@ "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==", + "license": "MIT", "optional": true }, "node_modules/gauge": { @@ -6253,25 +5224,26 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/gaxios": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", - "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", - "optional": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" }, "engines": { "node": ">=14" } }, "node_modules/gaxios/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "optional": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -6280,10 +5252,10 @@ } }, "node_modules/gaxios/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "optional": true, + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -6292,11 +5264,24 @@ "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==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "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==", - "optional": true, + "license": "Apache-2.0", "dependencies": { "gaxios": "^6.0.0", "json-bigint": "^1.0.0" @@ -6415,10 +5400,10 @@ } }, "node_modules/google-auth-library": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.5.0.tgz", - "integrity": "sha512-OUbP509lWVlZxuMY+Cgomw49VzZFP9myIcVeYEpeBlbXJbPC4R+K4BmO9hd3ciYM5QIwm5W1PODcKjqxtkye9Q==", - "optional": true, + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz", + "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==", + "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -6432,21 +5417,22 @@ } }, "node_modules/google-gax": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.4.tgz", - "integrity": "sha512-upnobdflCz9+Lq9+nOv0pm9EQ+fLhWckz6lQTgLAkLAGggIH2fl+CUj0WgczdbhQDAnA0BSNfXYHglhA/dmZpw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@grpc/grpc-js": "~1.10.3", - "@grpc/proto-loader": "^0.7.0", + "@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.6.1", + "node-fetch": "^2.7.0", "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.0", - "protobufjs": "7.3.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", "retry-request": "^7.0.0", "uuid": "^9.0.1" }, @@ -6462,6 +5448,7 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -6505,10 +5492,10 @@ } }, "node_modules/gtoken": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", - "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", - "optional": true, + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -6617,6 +5604,23 @@ "remote-content": "^3.0.1" } }, + "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" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -6653,7 +5657,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==" + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "4.0.1", @@ -6691,26 +5696,6 @@ "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==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6743,9 +5728,10 @@ } }, "node_modules/intuit-oauth": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/intuit-oauth/-/intuit-oauth-4.1.2.tgz", - "integrity": "sha512-ABmJS0dJsjgM9VXo9LRAuxo5y2J19OsOuHqf5TR3KUxI1swpser6G1jEz6RZ51K+YyhzI1KPCFAwdYxlNhWSaw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/intuit-oauth/-/intuit-oauth-4.1.3.tgz", + "integrity": "sha512-jamanOys33Z2Uw1bisf+v7M+2rE9syMPmPZzkOt0iUrTp+IUk99QLgirKPukXidfwVdhgURnkTVb2wUcp84D8g==", + "license": "Apache-2.0", "dependencies": { "atob": "2.1.2", "axios": "^1.5.1", @@ -6993,9 +5979,10 @@ } }, "node_modules/json11": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/json11/-/json11-1.1.2.tgz", - "integrity": "sha512-5r1RHT1/Gr/jsI/XZZj/P6F11BKM8xvTaftRuiLkQI9Z2PFDukM82Ysxw8yDszb3NJP/NKnRlSGmhUdG99rlBw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json11/-/json11-2.0.0.tgz", + "integrity": "sha512-VuKJKUSPEJape+daTm70Nx7vdcdorf4S6LCyN2z0jUVH4UrQ4ftXo2kC0bnHpCREmxHuHqCNVPA75BjI3CB6Ag==", + "license": "MIT", "bin": { "json11": "dist/cli.mjs" } @@ -7052,7 +6039,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, + "license": "MIT", "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -7079,7 +6066,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, + "license": "MIT", "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -7131,6 +6118,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", "optional": true }, "node_modules/lodash.clonedeep": { @@ -7204,9 +6192,9 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", @@ -7224,6 +6212,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0", "optional": true }, "node_modules/lru-cache": { @@ -7319,6 +6308,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", "optional": true, "bin": { "mime": "cli.js" @@ -7555,9 +6545,9 @@ } }, "node_modules/nodemailer": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", - "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -7618,6 +6608,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6" @@ -7706,6 +6697,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "optional": true, "dependencies": { "yocto-queue": "^0.1.0" @@ -7832,9 +6824,9 @@ "license": "MIT" }, "node_modules/phone": { - "version": "3.1.51", - "resolved": "https://registry.npmjs.org/phone/-/phone-3.1.51.tgz", - "integrity": "sha512-ggy58LoEAb6LPZEyz1iXz7dQKbDAwp6V6yQgRQUDSlx4HgKm2YGETA5Ns/39/HQ+QL74+OBR05xzdUhi6TMHLQ==", + "version": "3.1.53", + "resolved": "https://registry.npmjs.org/phone/-/phone-3.1.53.tgz", + "integrity": "sha512-+0sMjlxjcm1rjUDRLzXW06vRg/SePwa+MubuSt9WhHoUziGrGHjuC/tfFYfh2oXKU/dcckwCyMUMDAOaVArb6w==", "license": "MIT", "engines": { "node": ">=12" @@ -7871,15 +6863,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "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==", - "license": "MIT", - "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", @@ -7909,6 +6892,7 @@ "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==", + "license": "Apache-2.0", "optional": true, "dependencies": { "protobufjs": "^7.2.5" @@ -7918,10 +6902,11 @@ } }, "node_modules/protobufjs": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", - "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, + "license": "BSD-3-Clause", "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -8161,6 +7146,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", "optional": true, "engines": { "node": ">= 4" @@ -8170,6 +7156,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", "optional": true, "dependencies": { "@types/request": "^2.48.8", @@ -8479,9 +7466,9 @@ } }, "node_modules/soap": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/soap/-/soap-1.1.5.tgz", - "integrity": "sha512-6YJrwY+tXHwlk/wtS7+XSc0jyEWgNw8xJQYvY9m1jZlPaGkc2nzmwKAq98fwGIw51acywhsraaeq/6GFggaNYw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/soap/-/soap-1.1.6.tgz", + "integrity": "sha512-em3PDqr5kQjzDRkWRQ4JMCPg32uMonSdLds0QgRJrJBLid1/LHdhUgQuPxJA6SFV1/58Wu7HWIypmW+vqmUPlw==", "license": "MIT", "dependencies": { "axios": "^1.7.7", @@ -8500,9 +7487,9 @@ } }, "node_modules/socket.io": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", - "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -8796,6 +7783,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", "optional": true, "dependencies": { "stubs": "^3.0.0" @@ -8805,6 +7793,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", "optional": true }, "node_modules/streamsearch": { @@ -8911,12 +7900,14 @@ "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", "optional": true }, "node_modules/style-data": { @@ -9043,6 +8034,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", "optional": true, "dependencies": { "http-proxy-agent": "^5.0.0", @@ -9059,6 +8051,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", "optional": true, "engines": { "node": ">= 10" @@ -9068,6 +8061,7 @@ "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==", + "license": "MIT", "optional": true, "dependencies": { "@tootallnate/once": "2", @@ -9086,6 +8080,7 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -9288,9 +8283,9 @@ } }, "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==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, "node_modules/universalify": { @@ -9381,6 +8376,7 @@ "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -9394,6 +8390,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } @@ -9438,22 +8435,22 @@ } }, "node_modules/winston": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", - "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.6.0", + "logform": "^2.7.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" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" @@ -9479,35 +8476,19 @@ } }, "node_modules/winston-transport": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.8.0.tgz", - "integrity": "sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "license": "MIT", "dependencies": { - "logform": "^2.6.1", - "readable-stream": "^4.5.2", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { "node": ">= 12.0.0" } }, - "node_modules/winston-transport/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==", - "license": "MIT", - "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/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9700,6 +8681,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "optional": true, "engines": { "node": ">=10" diff --git a/package.json b/package.json index f51af45b5..a0b146f14 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ "makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"" }, "dependencies": { - "@aws-sdk/client-cloudwatch-logs": "^3.679.0", - "@aws-sdk/client-elasticache": "^3.675.0", - "@aws-sdk/client-s3": "^3.689.0", - "@aws-sdk/client-secrets-manager": "^3.675.0", - "@aws-sdk/client-ses": "^3.675.0", - "@aws-sdk/credential-provider-node": "^3.675.0", - "@opensearch-project/opensearch": "^2.12.0", + "@aws-sdk/client-cloudwatch-logs": "^3.693.0", + "@aws-sdk/client-elasticache": "^3.693.0", + "@aws-sdk/client-s3": "^3.693.0", + "@aws-sdk/client-secrets-manager": "^3.693.0", + "@aws-sdk/client-ses": "^3.693.0", + "@aws-sdk/credential-provider-node": "^3.693.0", + "@opensearch-project/opensearch": "^2.13.0", "@socket.io/admin-ui": "^0.5.1", "@socket.io/redis-adapter": "^8.3.0", "aws4": "^1.13.2", @@ -34,20 +34,20 @@ "bluebird": "^3.7.2", "body-parser": "^1.20.3", "canvas": "^2.11.2", - "chart.js": "^4.4.5", + "chart.js": "^4.4.6", "cloudinary": "^2.5.1", - "compression": "^1.7.4", + "compression": "^1.7.5", "cookie-parser": "^1.4.7", "cors": "2.8.5", "csrf": "^3.1.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", - "firebase-admin": "^12.6.0", + "firebase-admin": "^13.0.0", "graphql": "^16.9.0", "graphql-request": "^6.1.0", "inline-css": "^4.0.2", - "intuit-oauth": "^4.1.2", + "intuit-oauth": "^4.1.3", "ioredis": "^5.4.1", "json-2-csv": "^5.5.6", "lodash": "^4.17.21", @@ -56,18 +56,18 @@ "multer": "^1.4.5-lts.1", "node-mailjet": "^6.0.6", "node-persist": "^4.0.3", - "nodemailer": "^6.9.15", - "phone": "^3.1.51", + "nodemailer": "^6.9.16", + "phone": "^3.1.53", "recursive-diff": "^1.0.9", "redis": "^4.7.0", "rimraf": "^6.0.1", - "soap": "^1.1.5", - "socket.io": "^4.8.0", + "soap": "^1.1.6", + "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", "ssh2-sftp-client": "^10.0.3", "twilio": "^4.23.0", "uuid": "^10.0.0", - "winston": "^3.15.0", + "winston": "^3.17.0", "winston-cloudwatch": "^6.3.0", "xml2js": "^0.6.2", "xmlbuilder2": "^3.1.1" diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 99e37bebe..0a2b6a853 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -87,6 +87,21 @@ mutation RECEIVE_MESSAGE($msg: [messages_insert_input!]!) { updated_at unreadcnt phone_num + label + job_conversations { + job { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + } + } + messages_aggregate (where: { read: { _eq: false }, isoutbound: { _eq: false } }){ + aggregate { + count + } + } } conversationid created_at @@ -116,6 +131,7 @@ mutation INSERT_MESSAGE($msg: [messages_insert_input!]!, $conversationid: uuid!) id archived bodyshop { + id imexshopid } created_at @@ -144,6 +160,11 @@ mutation UPDATE_MESSAGE($msid: String!, $fields: messages_set_input!) { update_messages(where: { msid: { _eq: $msid } }, _set: $fields) { returning { id + status + conversationid + conversation{ + bodyshopid + } } } }`; @@ -2544,3 +2565,69 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) { } } `; + +exports.GET_CONVERSATIONS = `query GET_CONVERSATIONS($bodyshopId: uuid!) { + conversations( + where: { bodyshopid: { _eq: $bodyshopId }, archived: { _eq: false } }, + order_by: { updated_at: desc }, + limit: 50 + ) { + phone_num + id + updated_at + unreadcnt + archived + label + messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) { + aggregate { + count + } + } + job_conversations { + job { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + } + } + } +} +`; + +exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: uuid!) { + update_messages(where: { conversationid: { _eq: $conversationId } }, _set: { read: true }) { + affected_rows + } +} +`; + +exports.GET_CONVERSATION_DETAILS = ` + query GET_CONVERSATION_DETAILS($conversationId: uuid!) { + conversation: conversations_by_pk(id: $conversationId) { + id + phone_num + updated_at + label + job_conversations { + job { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + } + } + } + messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) { + id + text + created_at + read + isoutbound + userid + image_path + } + } +`; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index ff643a4b0..7463e1757 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -13,6 +13,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails"); const { canvastest } = require("../render/canvas-handler"); const { alertCheck } = require("../alerts/alertcheck"); +const uuid = require("uuid").v4; //Test route to ensure Express is responding. router.get("/test", eventAuthorizationMiddleware, async function (req, res) { @@ -57,6 +58,59 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => { return res.status(500).send("Logs tested."); }); +router.get("/wstest", eventAuthorizationMiddleware, (req, res) => { + const { ioRedis } = req; + ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", { + isoutbound: true, + conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6", + msid: "SM5d053957bc0da29399b768c23bffcc0f", + summary: true + }); + + // TODO: Do we need to add more content here? + ioRedis + .to(`bodyshop-conversation-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582:2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6`) + .emit("new-message-detailed", { + // + // msid: "SMbbd7703a898fef7f2c07c148ade8a6cd", + // text: "test2", + // conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6", + // isoutbound: true, + // userid: "patrick@imex.dev", + // image: false, + // image_path: [], + newMessage: { + conversation: { + id: uuid(), + archived: false, + bodyshop: { + id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582", + imexshopid: "APPLE" + }, + created_at: "2024-11-19T19:46:38.984633+00:00", + updated_at: "2024-11-19T22:40:48.346875+00:00", + unreadcnt: 0, + phone_num: "+16138676684" + }, + conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6", + created_at: "2024-11-19T22:40:48.346875+00:00", + id: "68604ea9-c411-43ec-ab83-899868e58819", + image_path: [], + image: false, + isoutbound: true, + msid: "SMbbd7703a898fef7f2c07c148ade8a6cd", + read: false, + text: `This is a test ${Math.round(Math.random() * 100)}`, + updated_at: "2024-11-19T22:40:48.346875+00:00", + status: "posted", + userid: "patrick@imex.dev" + }, + conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6", + summary: false + }); // TODO: Do we need to add more content here? + + return res.status(500).send("Logs tested."); +}); // Search router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search); diff --git a/server/sms/receive.js b/server/sms/receive.js index c34147833..72dcb0c39 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -11,7 +11,11 @@ const logger = require("../utils/logger"); const InstanceManager = require("../utils/instanceMgr").default; exports.receive = async (req, res) => { - //Perform request validation + // Perform request validation + const { + ioRedis, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } + } = req; logger.log("sms-inbound", "DEBUG", "api", null, { msid: req.body.SmsMessageSid, @@ -20,7 +24,7 @@ exports.receive = async (req, res) => { image_path: generateMediaArray(req.body) }); - if (!!!req.body || !!!req.body.MessagingServiceSid || !!!req.body.SmsMessageSid) { + if (!req.body || !req.body.MessagingServiceSid || !req.body.SmsMessageSid) { logger.log("sms-inbound-error", "ERROR", "api", null, { msid: req.body.SmsMessageSid, text: req.body.Body, @@ -28,170 +32,195 @@ exports.receive = async (req, res) => { image_path: generateMediaArray(req.body), type: "malformed-request" }); - res.status(400); - res.json({ success: false, error: "Malformed Request" }); - } else { - try { - const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { - mssid: req.body.MessagingServiceSid, - phone: phone(req.body.From).phoneNumber - }); + res.status(400).json({ success: false, error: "Malformed Request" }); + return; + } - let newMessage = { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body) - }; - if (response.bodyshops[0]) { - //Found a bodyshop - should always happen. - if (response.bodyshops[0].conversations.length === 0) { - //No conversation Found, create one. - //console.log("[SMS Receive] No conversation found. Creating one."); - newMessage.conversation = { - data: { - bodyshopid: response.bodyshops[0].id, - phone_num: phone(req.body.From).phoneNumber - } - }; - } else if (response.bodyshops[0].conversations.length === 1) { - //Just add it to the conversation - //console.log("[SMS Receive] Conversation found. Added ID."); - newMessage.conversationid = response.bodyshops[0].conversations[0].id; - } else { - //We should never get here. - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - type: "duplicate-phone" - }); - } - try { - let insertresp; - if (response.bodyshops[0].conversations[0]) { - insertresp = await client.request(queries.INSERT_MESSAGE, { - msg: newMessage, - conversationid: response.bodyshops[0].conversations[0] && response.bodyshops[0].conversations[0].id - }); - } else { - insertresp = await client.request(queries.RECEIVE_MESSAGE, { - msg: newMessage - }); + try { + const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { + mssid: req.body.MessagingServiceSid, + phone: phone(req.body.From).phoneNumber + }); + + let newMessage = { + msid: req.body.SmsMessageSid, + text: req.body.Body, + image: !!req.body.MediaUrl0, + image_path: generateMediaArray(req.body) + }; + + if (response.bodyshops[0]) { + if (response.bodyshops[0].conversations.length === 0) { + // No conversation found, create one + newMessage.conversation = { + data: { + bodyshopid: response.bodyshops[0].id, + phone_num: phone(req.body.From).phoneNumber } - const message = insertresp.insert_messages.returning[0]; - const data = { - type: "messaging-inbound", - conversationid: message.conversationid || "", - text: message.text || "", - messageid: message.id || "", - phone_num: message.conversation.phone_num || "" - }; + }; - const fcmresp = await admin.messaging().send({ - topic: `${message.conversation.bodyshop.imexshopid}-messaging`, - notification: { - title: InstanceManager({ - imex: `ImEX Online Message - ${data.phone_num}`, - rome: `Rome Online Message - ${data.phone_num}`, - promanager: `ProManager Message - ${data.phone_num}` - }), - body: message.image_path ? `Image ${message.text}` : message.text - //imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances - }, - data + try { + // Insert new conversation and message + const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage }); + + // Safely access conversation and message + const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null; + const message = insertresp?.insert_messages?.returning?.[0]; + + if (!createdConversation) { + throw new Error("Conversation data is missing from the response."); + } + + const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: message.conversation.bodyshop.id, + conversationId: message.conversation.id + }); + // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { + isoutbound: false, + existingConversation: false, + newConversation: createdConversation, + conversationId: createdConversation.id, + updated_at: message.updated_at, + msid: message.sid, + summary: true + }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: message, + isoutbound: false, + newConversation: createdConversation, + existingConversation: false, + conversationId: createdConversation.id, + summary: false }); logger.log("sms-inbound-success", "DEBUG", "api", null, { newMessage, - fcmresp + createdConversation }); + res.status(200).send(""); - } catch (e2) { + return; + } catch (e) { logger.log("sms-inbound-error", "ERROR", "api", null, { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body), messagingServiceSid: req.body.MessagingServiceSid, - error: e2 + error: e }); - res.sendStatus(500).json(e2); + res.status(500).json(e); + return; } + } else if (response.bodyshops[0].conversations.length === 1) { + // Add to the existing conversation + // conversation UPDATED + newMessage.conversationid = response.bodyshops[0].conversations[0].id; + } else { + // Duplicate phone error + logger.log("sms-inbound-error", "ERROR", "api", null, { + msid: req.body.SmsMessageSid, + text: req.body.Body, + image: !!req.body.MediaUrl0, + image_path: generateMediaArray(req.body), + messagingServiceSid: req.body.MessagingServiceSid, + type: "duplicate-phone" + }); + res.status(400).json({ success: false, error: "Duplicate phone number" }); + return; + } + + try { + // Insert message into an existing conversation + const insertresp = await client.request(queries.INSERT_MESSAGE, { + msg: newMessage, + conversationid: newMessage.conversationid + }); + + const message = insertresp.insert_messages.returning[0]; + const data = { + type: "messaging-inbound", + conversationid: message.conversationid || "", + text: message.text || "", + messageid: message.id || "", + phone_num: message.conversation.phone_num || "" + }; + + const fcmresp = await admin.messaging().send({ + topic: `${message.conversation.bodyshop.imexshopid}-messaging`, + notification: { + title: InstanceManager({ + imex: `ImEX Online Message - ${data.phone_num}`, + rome: `Rome Online Message - ${data.phone_num}`, + promanager: `ProManager Message - ${data.phone_num}` + }), + body: message.image_path ? `Image ${message.text}` : message.text + }, + data + }); + + logger.log("sms-inbound-success", "DEBUG", "api", null, { + newMessage, + fcmresp + }); + + const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: message.conversation.bodyshop.id, + conversationId: message.conversation.id + }); + // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { + isoutbound: false, + existingConversation: true, + conversationId: conversationid, + updated_at: message.updated_at, + msid: message.sid, + summary: true + }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: message, + isoutbound: false, + existingConversation: true, + conversationId: conversationid, + summary: false + }); + + res.status(200).send(""); + } catch (e) { + logger.log("sms-inbound-error", "ERROR", "api", null, { + msid: req.body.SmsMessageSid, + text: req.body.Body, + image: !!req.body.MediaUrl0, + image_path: generateMediaArray(req.body), + messagingServiceSid: req.body.MessagingServiceSid, + error: e + }); + + res.status(500).json(e); } - } catch (e1) { - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - error: e1 - }); - res.sendStatus(500).json(e1); } + } catch (e) { + logger.log("sms-inbound-error", "ERROR", "api", null, { + msid: req.body.SmsMessageSid, + text: req.body.Body, + image: !!req.body.MediaUrl0, + image_path: generateMediaArray(req.body), + messagingServiceSid: req.body.MessagingServiceSid, + error: e + }); + res.status(500).json(e); } }; -// const sampleMessage: { -// "ToCountry": "CA", -// "ToState": "BC", -// "SmsMessageSid": "SMad7bddaf3454c0904999d6018b1e8f49", -// "NumMedia": "0", -// "ToCity": "Vancouver", -// "FromZip": "", -// "SmsSid": "SMad7bddaf3454c0904999d6018b1e8f49", -// "FromState": "BC", -// "SmsStatus": "received", -// "FromCity": "VANCOUVER", -// "Body": "Hi", -// "FromCountry": "CA", -// "To": "+16043301606", -// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1", -// "ToZip": "", -// "NumSegments": "1", -// "MessageSid": "SMad7bddaf3454c0904999d6018b1e8f49", -// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c", -// "From": "+16049992002", -// "ApiVersion": "2010-04-01" -// } -// ] req.body { -// [0] ToCountry: 'CA', -// [0] MediaContentType0: 'image/jpeg', -// [0] ToState: 'BC', -// [0] SmsMessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27', -// [0] NumMedia: '1', -// [0] ToCity: 'Vancouver', -// [0] FromZip: '', -// [0] SmsSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27', -// [0] FromState: 'BC', -// [0] SmsStatus: 'received', -// [0] FromCity: 'VANCOUVER', -// [0] Body: '', -// [0] FromCountry: 'CA', -// [0] To: '+16043301606', -// [0] MessagingServiceSid: 'MG6e259e2add04ffa0d0aa355038670ee1', -// [0] ToZip: '', -// [0] NumSegments: '1', -// [0] MessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27', -// [0] AccountSid: 'AC6c09d337d6b9c68ab6488c2052bd457c', -// [0] From: '+16049992002', -// [0] MediaUrl0: 'https://api.twilio.com/2010-04-01/Accounts/AC6c09d337d6b9c68ab6488c2052bd457c/Messages/MM14fa2851ba26e0dc2b62073f8e7cdf27/Media/MEf129dd37979852f395eb29ffb126e19e', -// [0] ApiVersion: '2010-04-01' -// [0] } - -// [0] MediaContentType0: 'image/jpeg', -// MediaContentType0: 'video/3gpp', - const generateMediaArray = (body) => { const { NumMedia } = body; if (parseInt(NumMedia) > 0) { - //stuff const ret = []; - for (var i = 0; i < parseInt(NumMedia); i++) { + for (let i = 0; i < parseInt(NumMedia); i++) { ret.push(body[`MediaUrl${i}`]); } return ret; diff --git a/server/sms/send.js b/server/sms/send.js index e86966342..7975cc2f9 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -9,11 +9,14 @@ const queries = require("../graphql-client/queries"); const logger = require("../utils/logger"); const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); const { admin } = require("../firebase/firebase-handler"); - const gqlClient = require("../graphql-client/graphql-client").client; exports.send = (req, res) => { const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body; + const { + ioRedis, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } + } = req; logger.log("sms-outbound", "DEBUG", req.user.email, null, { messagingServiceSid: messagingServiceSid, @@ -59,18 +62,36 @@ exports.send = (req, res) => { conversationid: newMessage.conversationid || "" }; - admin.messaging().send({ - topic: `${imexshopid}-messaging`, - data + // TODO Verify + // const messageData = response.insert_messages.returning[0]; + + // Broadcast new message to conversation room + const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: r2.insert_messages.returning[0].conversation.bodyshop.id, + conversationId: r2.insert_messages.returning[0].conversation.id }); + ioRedis.to(broadcastRoom).emit("new-message-summary", { + isoutbound: true, + conversationId: conversationid, + updated_at: r2.insert_messages.returning[0].updated_at, + msid: message.sid, + summary: true + }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: r2.insert_messages.returning[0], + conversationId: conversationid, + summary: false + }); res.sendStatus(200); }) .catch((e2) => { logger.log("sms-outbound-error", "ERROR", req.user.email, null, { msid: message.sid, conversationid, - error: e2 + error: e2.message, + stack: e2.stack }); //res.json({ success: false, message: e2 }); @@ -80,7 +101,8 @@ exports.send = (req, res) => { //res.json({ success: false, message: error }); logger.log("sms-outbound-error", "ERROR", req.user.email, null, { conversationid, - error: e1 + error: e1.message, + stack: e1.stack }); }); } else { diff --git a/server/sms/status.js b/server/sms/status.js index 9b5aeb733..63c45b33e 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -11,6 +11,10 @@ const { admin } = require("../firebase/firebase-handler"); exports.status = (req, res) => { const { SmsSid, SmsStatus } = req.body; + const { + ioRedis, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } + } = req; client .request(queries.UPDATE_MESSAGE_STATUS, { msid: SmsSid, @@ -21,6 +25,17 @@ exports.status = (req, res) => { msid: SmsSid, fields: { status: SmsStatus } }); + // TODO Verify + const message = response.update_messages.returning[0]; + + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: message.conversation.bodyshopid, + conversationId: message.conversationid + }); + + ioRedis.to(conversationRoom).emit("message-changed", { + message + }); }) .catch((error) => { logger.log("sms-status-update-error", "ERROR", "api", null, { @@ -34,18 +49,44 @@ exports.status = (req, res) => { exports.markConversationRead = async (req, res) => { const { conversationid, imexshopid } = req.body; - admin.messaging().send({ - topic: `${imexshopid}-messaging`, - // notification: { - // title: `ImEX Online Message - ${data.phone_num}`, - // body: message.image_path ? `Image ${message.text}` : message.text, - // imageUrl: "https://thinkimex.com/img/logo512.png", - // }, - data: { - type: "messaging-mark-conversation-read", - conversationid: conversationid || "" - } + const { + ioRedis, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } + } = req; + + //Server side, mark the conversation as read + + //TODO: Convert this to run on server side. Stolen from chat-conversation.container.jsx + // const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, { + // variables: { conversationId: selectedConversation }, + // refetchQueries: ["UNREAD_CONVERSATION_COUNT"], + // + // update(cache) { + // cache.modify({ + // id: cache.identify({ + // __typename: "conversations", + // id: selectedConversation + // }), + // fields: { + // messages_aggregate(cached) { + // return { aggregate: { count: 0 } }; + // } + // } + // }); + // } + // }); + + const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + + ioRedis.to(broadcastRoom).emit("conversation-changed", { + //type: "conversation-marked-unread" //TODO: Flush out what this looks like. + // isoutbound: true, + // conversationId: conversationid, + // updated_at: r2.insert_messages.returning[0].updated_at, + // msid: message.sid, + // summary: true }); + res.send(200); }; diff --git a/server/utils/ioHelpers.js b/server/utils/ioHelpers.js index 3b3b15adb..a95bd90b0 100644 --- a/server/utils/ioHelpers.js +++ b/server/utils/ioHelpers.js @@ -1,8 +1,12 @@ const applyIOHelpers = ({ app, api, io, logger }) => { const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`; + // Messaging - conversation specific room to handle detailed messages when the user has a conversation open. + const getBodyshopConversationRoom = ({bodyshopId, conversationId}) => + `bodyshop-conversation-room:${bodyshopId}:${conversationId}`; const ioHelpersAPI = { - getBodyshopRoom + getBodyshopRoom, + getBodyshopConversationRoom }; // Helper middleware diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 7ae8cd8c4..51e8039d8 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -1,9 +1,17 @@ const { admin } = require("../firebase/firebase-handler"); +const { MARK_MESSAGES_AS_READ, GET_CONVERSATIONS, GET_CONVERSATION_DETAILS } = require("../graphql-client/queries"); +const { phone } = require("phone"); +const { client: gqlClient } = require("../graphql-client/graphql-client"); +const queries = require("../graphql-client/queries"); +const twilio = require("twilio"); +const client = require("../graphql-client/graphql-client").client; + +const twilioClient = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); const redisSocketEvents = ({ io, redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis - ioHelpers: { getBodyshopRoom }, + ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }, logger }) => { // Logging helper functions @@ -113,6 +121,7 @@ const redisSocketEvents = ({ socket.on("leave-bodyshop-room", leaveBodyshopRoom); socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom); }; + // Disconnect Events const registerDisconnectEvents = (socket) => { const disconnect = () => { @@ -129,10 +138,37 @@ const redisSocketEvents = ({ socket.on("disconnect", disconnect); }; + // Messaging Events + const registerMessagingEvents = (socket) => { + const joinConversationRoom = async ({ bodyshopId, conversationId }) => { + try { + const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + socket.join(room); + } catch (error) { + logger.log("error", "Failed to join conversation", error); + socket.emit("error", { message: "Failed to join conversation" }); + } + }; + + const leaveConversationRoom = ({ bodyshopId, conversationId }) => { + try { + const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + socket.leave(room); + // Optionally notify the client + //socket.emit("conversation-left", { conversationId }); + } catch (error) { + socket.emit("error", { message: "Failed to leave conversation" }); + } + }; + + socket.on("join-bodyshop-conversation", joinConversationRoom); + socket.on("leave-bodyshop-conversation", leaveConversationRoom); + }; // Call Handlers registerRoomAndBroadcastEvents(socket); registerUpdateEvents(socket); + registerMessagingEvents(socket); registerDisconnectEvents(socket); }; From 261353b51113f11737cc61ac1717461f2a689e95 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 11:35:30 -0800 Subject: [PATCH 02/30] feature/IO-3000-messaging-sockets-migrations2 - Base cleanup Signed-off-by: Dave Richer --- .../chat-affix/chat-affix.container.jsx | 79 +-------- .../registerMessagingSocketHandlers.js | 27 ++-- .../chat-conversation.container.jsx | 15 +- .../profile-shops/profile-shops.container.jsx | 2 +- client/src/contexts/SocketIO/useSocket.js | 1 - client/src/utils/fcm-handler.js | 70 -------- server/sms/receive.js | 78 ++++----- server/sms/send.js | 151 ++++++++---------- server/web-sockets/redisSocketEvents.js | 27 ++-- 9 files changed, 145 insertions(+), 305 deletions(-) delete mode 100644 client/src/utils/fcm-handler.js diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 0c13d8327..951783f8a 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -1,11 +1,6 @@ import { useApolloClient } from "@apollo/client"; -import { getToken, onMessage } from "@firebase/messaging"; -import { Button, notification, Space } from "antd"; -import axios from "axios"; import React, { useContext, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { messaging, requestForToken } from "../../firebase/firebase.utils"; -import FcmHandler from "../../utils/fcm-handler"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; import SocketContext from "../../contexts/SocketIO/socketContext"; @@ -20,81 +15,17 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { if (!bodyshop || !bodyshop.messagingservicesid) return; //Register WS handlers - registerMessagingHandlers({ socket, client }); - - async function SubscribeToTopic() { - try { - const r = await axios.post("/notifications/subscribe", { - fcm_tokens: await getToken(messaging, { - vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY - }), - type: "messaging", - imexshopid: bodyshop.imexshopid - }); - console.log("FCM Topic Subscription", r.data); - } catch (error) { - console.log("Error attempting to subscribe to messaging topic: ", error); - notification.open({ - key: "fcm", - type: "warning", - message: t("general.errors.fcm"), - btn: ( - - - - - ) - }); - } + if (socket && socket.connected) { + registerMessagingHandlers({ socket, client }); } - SubscribeToTopic(); - // eslint-disable-next-line react-hooks/exhaustive-deps - return () => { - unregisterMessagingHandlers({ socket }); + if (socket && socket.connected) { + unregisterMessagingHandlers({ socket }); + } }; }, [bodyshop, socket, t, client]); - useEffect(() => { - function handleMessage(payload) { - FcmHandler({ - client, - payload: (payload && payload.data && payload.data.data) || payload.data - }); - } - - let stopMessageListener, channel; - try { - stopMessageListener = onMessage(messaging, handleMessage); - channel = new BroadcastChannel("imex-sw-messages"); - channel.addEventListener("message", handleMessage); - } catch (error) { - console.log("Unable to set event listeners."); - } - return () => { - stopMessageListener && stopMessageListener(); - channel && channel.removeEventListener("message", handleMessage); - }; - }, [client]); - if (!bodyshop || !bodyshop.messagingservicesid) return <>; return ( diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 08d2e86bb..c6cd66c0e 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -1,8 +1,9 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; -export function registerMessagingHandlers({ socket, client }) { +export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; - function handleNewMessageSummary(message) { + + const handleNewMessageSummary = (message) => { console.log("🚀 ~ SUMMARY CONSOLE LOG:", message); if (!message.isoutbound) { @@ -69,9 +70,9 @@ export function registerMessagingHandlers({ socket, client }) { } }); } - } + }; - function handleNewMessageDetailed(message) { + const handleNewMessageDetailed = (message) => { console.log("🚀 ~ DETAIL CONSOLE LOG:", message); //They're looking at the conversation right now. Need to merge into the list of messages i.e. append to the end. //Add the message to the overall cache. @@ -97,9 +98,9 @@ export function registerMessagingHandlers({ socket, client }) { // We got this as a receive. else { } - } + }; - function handleMessageChanged(message) { + const handleMessageChanged = (message) => { //Find it in the cache, and just update it based on what was sent. client.cache.modify({ id: client.cache.identify({ @@ -114,23 +115,23 @@ export function registerMessagingHandlers({ socket, client }) { } } }); - } + }; - function handleConversationChanged(conversation) { + const handleConversationChanged = (conversation) => { //If it was archived, marked unread, etc. - } + }; socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); - socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. -} + socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. +}; -export function unregisterMessagingHandlers({ socket }) { +export const unregisterMessagingHandlers = ({ socket }) => { if (!socket) return; socket.off("new-message-summary"); socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("message-changed"); socket.off("conversation-changed"); -} +}; diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 2ea760e3c..9a57a43e2 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,11 +1,10 @@ -import { useMutation, useQuery } from "@apollo/client"; +import { useQuery } from "@apollo/client"; import axios from "axios"; -import React, { useEffect, useState, useContext } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import SocketContext from "../../contexts/SocketIO/socketContext"; import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; -import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatConversationComponent from "./chat-conversation.component"; @@ -31,10 +30,16 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { const { socket } = useContext(SocketContext); useEffect(() => { - socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + socket.emit("join-bodyshop-conversation", { + bodyshopId: bodyshop.id, + conversationId: selectedConversation + }); return () => { - socket.emit("leave-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation }); + socket.emit("leave-bodyshop-conversation", { + bodyshopId: bodyshop.id, + conversationId: selectedConversation + }); }; }, [selectedConversation, bodyshop, socket]); diff --git a/client/src/components/profile-shops/profile-shops.container.jsx b/client/src/components/profile-shops/profile-shops.container.jsx index 4c5150913..46fe59aa2 100644 --- a/client/src/components/profile-shops/profile-shops.container.jsx +++ b/client/src/components/profile-shops/profile-shops.container.jsx @@ -5,7 +5,7 @@ import { QUERY_ALL_ASSOCIATIONS, UPDATE_ACTIVE_ASSOCIATION } from "../../graphql import AlertComponent from "../alert/alert.component"; import ProfileShopsComponent from "./profile-shops.component"; import axios from "axios"; -import { getToken } from "firebase/messaging"; +import { getToken } from " firebase/messaging"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 69c9402bd..885602fe3 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -67,7 +67,6 @@ const useSocket = (bodyshop) => { store.dispatch(setWssStatus("disconnected")); }; - //TODO: Check these handlers. socketInstance.on("connect", handleConnect); socketInstance.on("reconnect", handleReconnect); socketInstance.on("connect_error", handleConnectionError); diff --git a/client/src/utils/fcm-handler.js b/client/src/utils/fcm-handler.js deleted file mode 100644 index e3f7c8d43..000000000 --- a/client/src/utils/fcm-handler.js +++ /dev/null @@ -1,70 +0,0 @@ -export default async function FcmHandler({ client, payload }) { - console.log("FCM", payload); - // switch (payload.type) { - // case "messaging-inbound": - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // messages_aggregate(cached) { - // return { aggregate: { count: cached.aggregate.count + 1 } }; - // } - // } - // }); - // client.cache.modify({ - // fields: { - // messages_aggregate(cached) { - // return { aggregate: { count: cached.aggregate.count + 1 } }; - // } - // } - // }); - // break; - // case "messaging-outbound": - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // updated_at(oldupdated0) { - // return new Date(); - // } - // // messages_aggregate(cached) { - // // return { aggregate: { count: cached.aggregate.count + 1 } }; - // // }, - // } - // }); - // break; - // case "messaging-mark-conversation-read": - // let previousUnreadCount = 0; - // client.cache.modify({ - // id: client.cache.identify({ - // __typename: "conversations", - // id: payload.conversationid - // }), - // fields: { - // messages_aggregate(cached) { - // previousUnreadCount = cached.aggregate.count; - // return { aggregate: { count: 0 } }; - // } - // } - // }); - // client.cache.modify({ - // fields: { - // messages_aggregate(cached) { - // return { - // aggregate: { - // count: cached.aggregate.count - previousUnreadCount - // } - // }; - // } - // } - // }); - // break; - // default: - // console.log("No payload type set."); - // break; - // } -} diff --git a/server/sms/receive.js b/server/sms/receive.js index 72dcb0c39..3fb721eec 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -11,7 +11,6 @@ const logger = require("../utils/logger"); const InstanceManager = require("../utils/instanceMgr").default; exports.receive = async (req, res) => { - // Perform request validation const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } @@ -50,20 +49,17 @@ exports.receive = async (req, res) => { }; if (response.bodyshops[0]) { - if (response.bodyshops[0].conversations.length === 0) { - // No conversation found, create one + const bodyshop = response.bodyshops[0]; + if (bodyshop.conversations.length === 0) { newMessage.conversation = { data: { - bodyshopid: response.bodyshops[0].id, + bodyshopid: bodyshop.id, phone_num: phone(req.body.From).phoneNumber } }; try { - // Insert new conversation and message const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage }); - - // Safely access conversation and message const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null; const message = insertresp?.insert_messages?.returning?.[0]; @@ -71,12 +67,12 @@ exports.receive = async (req, res) => { throw new Error("Conversation data is missing from the response."); } - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const broadcastRoom = getBodyshopRoom(createdConversation.bodyshop.id); const conversationRoom = getBodyshopConversationRoom({ bodyshopId: message.conversation.bodyshop.id, conversationId: message.conversation.id }); - // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { isoutbound: false, existingConversation: false, @@ -86,6 +82,7 @@ exports.receive = async (req, res) => { msid: message.sid, summary: true }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, isoutbound: false, @@ -103,24 +100,12 @@ exports.receive = async (req, res) => { res.status(200).send(""); return; } catch (e) { - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - - res.status(500).json(e); + handleError(req, e, res, "RECEIVE_MESSAGE"); return; } - } else if (response.bodyshops[0].conversations.length === 1) { - // Add to the existing conversation - // conversation UPDATED - newMessage.conversationid = response.bodyshops[0].conversations[0].id; + } else if (bodyshop.conversations.length === 1) { + newMessage.conversationid = bodyshop.conversations[0].id; } else { - // Duplicate phone error logger.log("sms-inbound-error", "ERROR", "api", null, { msid: req.body.SmsMessageSid, text: req.body.Body, @@ -134,7 +119,6 @@ exports.receive = async (req, res) => { } try { - // Insert message into an existing conversation const insertresp = await client.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid: newMessage.conversationid @@ -167,52 +151,36 @@ exports.receive = async (req, res) => { fcmresp }); - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + const broadcastRoom = getBodyshopRoom(message.conversation.bodyshop.id); const conversationRoom = getBodyshopConversationRoom({ bodyshopId: message.conversation.bodyshop.id, conversationId: message.conversation.id }); - // Broadcast new message to the conversation room + ioRedis.to(broadcastRoom).emit("new-message-summary", { isoutbound: false, existingConversation: true, - conversationId: conversationid, + conversationId: message.conversationid, updated_at: message.updated_at, msid: message.sid, summary: true }); + ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, isoutbound: false, existingConversation: true, - conversationId: conversationid, + conversationId: message.conversationid, summary: false }); res.status(200).send(""); } catch (e) { - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - - res.status(500).json(e); + handleError(req, e, res, "INSERT_MESSAGE"); } } } catch (e) { - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - error: e - }); - res.status(500).json(e); + handleError(req, e, res, "FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID"); } }; @@ -228,3 +196,17 @@ const generateMediaArray = (body) => { return null; } }; + +const handleError = (req, error, res, context) => { + logger.log("sms-inbound-error", "ERROR", "api", null, { + msid: req.body.SmsMessageSid, + text: req.body.Body, + image: !!req.body.MediaUrl0, + image_path: generateMediaArray(req.body), + messagingServiceSid: req.body.MessagingServiceSid, + context, + error + }); + + res.status(500).json({ error: error.message || "Internal Server Error" }); +}; diff --git a/server/sms/send.js b/server/sms/send.js index 7975cc2f9..eb8cb6e5f 100644 --- a/server/sms/send.js +++ b/server/sms/send.js @@ -8,10 +8,9 @@ const { phone } = require("phone"); const queries = require("../graphql-client/queries"); const logger = require("../utils/logger"); const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); -const { admin } = require("../firebase/firebase-handler"); const gqlClient = require("../graphql-client/graphql-client").client; -exports.send = (req, res) => { +exports.send = async (req, res) => { const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body; const { ioRedis, @@ -19,7 +18,7 @@ exports.send = (req, res) => { } = req; logger.log("sms-outbound", "DEBUG", req.user.email, null, { - messagingServiceSid: messagingServiceSid, + messagingServiceSid, to: phone(to).phoneNumber, mediaUrl: selectedMedia.map((i) => i.src), text: body, @@ -30,85 +29,10 @@ exports.send = (req, res) => { image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] }); - if (!!to && !!messagingServiceSid && (!!body || !!selectedMedia.length > 0) && !!conversationid) { - client.messages - .create({ - body: body, - messagingServiceSid: messagingServiceSid, - to: phone(to).phoneNumber, - mediaUrl: selectedMedia.map((i) => i.src) - }) - .then((message) => { - let newMessage = { - msid: message.sid, - text: body, - conversationid, - isoutbound: true, - userid: req.user.email, - image: req.body.selectedMedia.length > 0, - image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] - }; - gqlClient - .request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid }) - .then((r2) => { - //console.log("Responding GQL Message ID", JSON.stringify(r2)); - logger.log("sms-outbound-success", "DEBUG", req.user.email, null, { - msid: message.sid, - conversationid - }); - - const data = { - type: "messaging-outbound", - conversationid: newMessage.conversationid || "" - }; - - // TODO Verify - // const messageData = response.insert_messages.returning[0]; - - // Broadcast new message to conversation room - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); - const conversationRoom = getBodyshopConversationRoom({ - bodyshopId: r2.insert_messages.returning[0].conversation.bodyshop.id, - conversationId: r2.insert_messages.returning[0].conversation.id - }); - - ioRedis.to(broadcastRoom).emit("new-message-summary", { - isoutbound: true, - conversationId: conversationid, - updated_at: r2.insert_messages.returning[0].updated_at, - msid: message.sid, - summary: true - }); - ioRedis.to(conversationRoom).emit("new-message-detailed", { - newMessage: r2.insert_messages.returning[0], - conversationId: conversationid, - summary: false - }); - res.sendStatus(200); - }) - .catch((e2) => { - logger.log("sms-outbound-error", "ERROR", req.user.email, null, { - msid: message.sid, - conversationid, - error: e2.message, - stack: e2.stack - }); - - //res.json({ success: false, message: e2 }); - }); - }) - .catch((e1) => { - //res.json({ success: false, message: error }); - logger.log("sms-outbound-error", "ERROR", req.user.email, null, { - conversationid, - error: e1.message, - stack: e1.stack - }); - }); - } else { + if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) { logger.log("sms-outbound-error", "ERROR", req.user.email, null, { type: "missing-parameters", - messagingServiceSid: messagingServiceSid, + messagingServiceSid, to: phone(to).phoneNumber, text: body, conversationid, @@ -118,5 +42,72 @@ exports.send = (req, res) => { image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] }); res.status(400).json({ success: false, message: "Missing required parameter(s)." }); + return; + } + + try { + const message = await client.messages.create({ + body, + messagingServiceSid, + to: phone(to).phoneNumber, + mediaUrl: selectedMedia.map((i) => i.src) + }); + + const newMessage = { + msid: message.sid, + text: body, + conversationid, + isoutbound: true, + userid: req.user.email, + image: req.body.selectedMedia.length > 0, + image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : [] + }; + + try { + const gqlResponse = await gqlClient.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid }); + + logger.log("sms-outbound-success", "DEBUG", req.user.email, null, { + msid: message.sid, + conversationid + }); + + const insertedMessage = gqlResponse.insert_messages.returning[0]; + const broadcastRoom = getBodyshopRoom(insertedMessage.conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: insertedMessage.conversation.bodyshop.id, + conversationId: insertedMessage.conversation.id + }); + + ioRedis.to(broadcastRoom).emit("new-message-summary", { + isoutbound: true, + conversationId: conversationid, + updated_at: insertedMessage.updated_at, + msid: message.sid, + summary: true + }); + + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: insertedMessage, + conversationId: conversationid, + summary: false + }); + + res.sendStatus(200); + } catch (gqlError) { + logger.log("sms-outbound-error", "ERROR", req.user.email, null, { + msid: message.sid, + conversationid, + error: gqlError.message, + stack: gqlError.stack + }); + res.status(500).json({ success: false, message: "Failed to insert message into database." }); + } + } catch (twilioError) { + logger.log("sms-outbound-error", "ERROR", req.user.email, null, { + conversationid, + error: twilioError.message, + stack: twilioError.stack + }); + res.status(500).json({ success: false, message: "Failed to send message through Twilio." }); } }; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 51e8039d8..8e1358994 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -1,12 +1,4 @@ const { admin } = require("../firebase/firebase-handler"); -const { MARK_MESSAGES_AS_READ, GET_CONVERSATIONS, GET_CONVERSATION_DETAILS } = require("../graphql-client/queries"); -const { phone } = require("phone"); -const { client: gqlClient } = require("../graphql-client/graphql-client"); -const queries = require("../graphql-client/queries"); -const twilio = require("twilio"); -const client = require("../graphql-client/graphql-client").client; - -const twilioClient = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY); const redisSocketEvents = ({ io, @@ -84,6 +76,7 @@ const redisSocketEvents = ({ }; socket.on("update-token", updateToken); }; + // Room Broadcast Events const registerRoomAndBroadcastEvents = (socket) => { const joinBodyshopRoom = (bodyshopUUID) => { @@ -138,6 +131,7 @@ const redisSocketEvents = ({ socket.on("disconnect", disconnect); }; + // Messaging Events const registerMessagingEvents = (socket) => { const joinConversationRoom = async ({ bodyshopId, conversationId }) => { @@ -145,19 +139,26 @@ const redisSocketEvents = ({ const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.join(room); } catch (error) { - logger.log("error", "Failed to join conversation", error); + logger.log("Failed to Join Conversation Room", "error", "io-redis", null, { + bodyshopId, + conversationId, + error: error.message, + stack: error.stack + }); socket.emit("error", { message: "Failed to join conversation" }); } }; - const leaveConversationRoom = ({ bodyshopId, conversationId }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); socket.leave(room); - // Optionally notify the client - //socket.emit("conversation-left", { conversationId }); } catch (error) { - socket.emit("error", { message: "Failed to leave conversation" }); + logger.log("Failed to Leave Conversation Room", "error", "io-redis", null, { + bodyshopId, + conversationId, + error: error.message, + stack: error.stack + }); } }; From e15384d0bf61dd9121f067de227708698c72eaea Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 12:23:50 -0800 Subject: [PATCH 03/30] feature/IO-3000-messaging-sockets-migrations2 - Everything but tagging and labels works Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 198 +++++++++++------- .../chat-conversation.container.jsx | 11 +- server/sms/status.js | 119 ++++++----- 3 files changed, 186 insertions(+), 142 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index c6cd66c0e..339acd9a6 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -4,127 +4,169 @@ export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; const handleNewMessageSummary = (message) => { - console.log("🚀 ~ SUMMARY CONSOLE LOG:", message); + const { conversationId, newConversation, existingConversation, isoutbound } = message; - if (!message.isoutbound) { - //It's an inbound message. - if (!message.existingConversation) { - //Do a read query. - const queryResults = client.cache.readQuery({ - query: CONVERSATION_LIST_QUERY, - variables: {} - }); - // Do a write query. Assume 0 unread messages to utilize code below. - client.cache.writeQuery({ - query: CONVERSATION_LIST_QUERY, - variables: {}, - data: { - conversations: [ - { ...message.newConversation, messages_aggregate: { aggregate: { count: 0 } } }, - ...queryResults - ] - } - }); - } + if (!existingConversation && newConversation?.phone_num) { + const queryResults = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 } + }); + + const fullConversation = { + ...newConversation, + phone_num: newConversation.phone_num, + id: newConversation.id, + updated_at: newConversation.updated_at || new Date().toISOString(), + unreadcnt: newConversation.unreadcnt || 0, + archived: newConversation.archived || false, + label: newConversation.label || null, + job_conversations: newConversation.job_conversations || [], + messages_aggregate: newConversation.messages_aggregate || { + aggregate: { count: isoutbound ? 0 : 1 } + } + }; + + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 }, + data: { + conversations: [fullConversation, ...(queryResults?.conversations || [])] + } + }); + } else { client.cache.modify({ id: client.cache.identify({ __typename: "conversations", - id: message.conversationId + id: conversationId }), fields: { updated_at: () => new Date(), messages_aggregate(cached) { - return { aggregate: { count: cached.aggregate.count + 1 } }; + // Increment unread count only if the message is inbound + if (!isoutbound) { + return { aggregate: { count: cached.aggregate.count + 1 } }; + } + return cached; } } }); - client.cache.modify({ - fields: { - conversations(existingConversations = [], { readField }) { - return [ - { __ref: `conversations:${message.conversationId}` }, // TODO: This throws the cache merging error in apollo. - ...existingConversations.filter((c) => c.__ref !== `conversations:${message.conversationId}`) - ]; - } - } - }); - - client.cache.modify({ - fields: { - messages_aggregate(cached) { - return { aggregate: { count: cached.aggregate.count + 1 } }; - } - } - }); - } else { - //It's an outbound message - //Update the last updated for conversations in the list. If it's new, add it in. - // If it isn't just update the last updated at. - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: message.conversationId - }), - fields: { - updated_at: () => message.newMessage.updated_at - } - }); } }; - const handleNewMessageDetailed = (message) => { - console.log("🚀 ~ DETAIL CONSOLE LOG:", message); - //They're looking at the conversation right now. Need to merge into the list of messages i.e. append to the end. - //Add the message to the overall cache. + const { conversationId, newMessage } = message; - //Handle outbound messages - if (message.newMessage.isoutbound) { - const queryResults = client.cache.readQuery({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId: message.newMessage.conversationid } - }); + // Append the new message to the conversation's message list + const queryResults = client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId } + }); + + if (queryResults) { client.cache.writeQuery({ query: GET_CONVERSATION_DETAILS, - variables: { conversationId: message.newMessage.conversationid }, + variables: { conversationId }, data: { ...queryResults, conversations_by_pk: { ...queryResults.conversations_by_pk, - messages: [...queryResults.conversations_by_pk.messages, message.newMessage] + messages: [...queryResults.conversations_by_pk.messages, newMessage] } } }); } - // We got this as a receive. - else { - } }; const handleMessageChanged = (message) => { - //Find it in the cache, and just update it based on what was sent. + // Find the message in the cache and update all fields dynamically client.cache.modify({ id: client.cache.identify({ __typename: "messages", id: message.id }), fields: { - //TODO: see if there is a way to have this update all fields e.g. only spread in updates rather than prescribing - updated_at: () => new Date(), - status(cached) { - return message.status; - } + // Dynamically update all fields based on the incoming message object + __typename: (existingType) => existingType || "messages", // Ensure __typename is preserved + ...Object.fromEntries( + Object.entries(message).map(([key, value]) => [ + key, + (cached) => (value !== undefined ? value : cached) // Update with new value or keep existing + ]) + ) } }); }; - const handleConversationChanged = (conversation) => { - //If it was archived, marked unread, etc. + const handleConversationChanged = (data) => { + const { type, conversationId, jobId, label } = data; + + switch (type) { + case "conversation-marked-read": + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fields: { + messages_aggregate: () => ({ aggregate: { count: 0 } }) + } + }); + + // Optionally, refetch queries if needed + // client.refetchQueries({ + // include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS] + // }); + break; + case "tag-added": + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fields: { + job_conversations(existingJobConversations = []) { + return [...existingJobConversations, { __ref: `jobs:${jobId}` }]; + } + } + }); + break; + + case "tag-removed": + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fields: { + job_conversations(existingJobConversations = []) { + return existingJobConversations.filter((jobRef) => jobRef.__ref !== `jobs:${jobId}`); + } + } + }); + break; + + case "label-changed": + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fields: { + label() { + return label; + } + } + }); + break; + + default: + console.warn(`Unhandled conversation change type: ${type}`); + } }; socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); - socket.on("conversation-changed", handleConversationChanged); //TODO: Unread, mark as read, archived, unarchive, etc. + socket.on("conversation-changed", handleConversationChanged); }; export const unregisterMessagingHandlers = ({ socket }) => { diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 9a57a43e2..58107ad7c 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -30,6 +30,9 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { const { socket } = useContext(SocketContext); useEffect(() => { + // Early gate, we have no socket, bail. + if (!socket || !socket.connected) return; + socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, conversationId: selectedConversation @@ -43,10 +46,6 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { }; }, [selectedConversation, bodyshop, socket]); - // const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, { - // variables: { conversationId: selectedConversation } - // }); - const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); const unreadCount = @@ -60,10 +59,10 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { const handleMarkConversationAsRead = async () => { if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) { setMarkingAsReadInProgress(true); - // await markConversationRead({}); await axios.post("/sms/markConversationRead", { conversationid: selectedConversation, - imexshopid: bodyshop.imexshopid + imexshopid: bodyshop.imexshopid, + bodyshopid: bodyshop.id }); setMarkingAsReadInProgress(false); } diff --git a/server/sms/status.js b/server/sms/status.js index 63c45b33e..9527ed5f8 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -5,29 +5,31 @@ require("dotenv").config({ const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); -const { phone } = require("phone"); const logger = require("../utils/logger"); -const { admin } = require("../firebase/firebase-handler"); -exports.status = (req, res) => { +exports.status = async (req, res) => { const { SmsSid, SmsStatus } = req.body; const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; - client - .request(queries.UPDATE_MESSAGE_STATUS, { + + try { + // Update message status in the database + const response = await client.request(queries.UPDATE_MESSAGE_STATUS, { msid: SmsSid, fields: { status: SmsStatus } - }) - .then((response) => { + }); + + const message = response.update_messages.returning[0]; + + if (message) { logger.log("sms-status-update", "DEBUG", "api", null, { msid: SmsSid, fields: { status: SmsStatus } }); - // TODO Verify - const message = response.update_messages.returning[0]; + // Emit WebSocket event to notify the change in message status const conversationRoom = getBodyshopConversationRoom({ bodyshopId: message.conversation.bodyshopid, conversationId: message.conversationid @@ -36,69 +38,70 @@ exports.status = (req, res) => { ioRedis.to(conversationRoom).emit("message-changed", { message }); - }) - .catch((error) => { - logger.log("sms-status-update-error", "ERROR", "api", null, { + } else { + logger.log("sms-status-update-warning", "WARN", "api", null, { msid: SmsSid, fields: { status: SmsStatus }, - error + warning: "No message returned from the database update." }); + } + res.sendStatus(200); + } catch (error) { + logger.log("sms-status-update-error", "ERROR", "api", null, { + msid: SmsSid, + fields: { status: SmsStatus }, + error }); - res.sendStatus(200); + res.status(500).json({ error: "Failed to update message status." }); + } }; exports.markConversationRead = async (req, res) => { - const { conversationid, imexshopid } = req.body; + const { conversationid, imexshopid, bodyshopid } = req.body; const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; - //Server side, mark the conversation as read + try { + // Mark messages in the conversation as read + const response = await client.request(queries.MARK_MESSAGES_AS_READ, { + conversationId: conversationid + }); - //TODO: Convert this to run on server side. Stolen from chat-conversation.container.jsx - // const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, { - // variables: { conversationId: selectedConversation }, - // refetchQueries: ["UNREAD_CONVERSATION_COUNT"], - // - // update(cache) { - // cache.modify({ - // id: cache.identify({ - // __typename: "conversations", - // id: selectedConversation - // }), - // fields: { - // messages_aggregate(cached) { - // return { aggregate: { count: 0 } }; - // } - // } - // }); - // } - // }); + const updatedMessages = response.update_messages.affected_rows; - const broadcastRoom = getBodyshopRoom(r2.insert_messages.returning[0].conversation.bodyshop.id); + logger.log("conversation-mark-read", "DEBUG", "api", null, { + conversationid, + imexshopid, + bodyshopid, + updatedMessages + }); - ioRedis.to(broadcastRoom).emit("conversation-changed", { - //type: "conversation-marked-unread" //TODO: Flush out what this looks like. - // isoutbound: true, - // conversationId: conversationid, - // updated_at: r2.insert_messages.returning[0].updated_at, - // msid: message.sid, - // summary: true - }); + const broadcastRoom = getBodyshopRoom(bodyshopid); - res.send(200); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: bodyshopid, + conversationId: conversationid + }); + + ioRedis.to(broadcastRoom).emit("conversation-changed", { + type: "conversation-marked-read", + conversationId: conversationid + }); + + ioRedis.to(conversationRoom).emit("message-changed", { + type: "all-messages-marked-read", + conversationId: conversationid + }); + + res.status(200).json({ success: true, message: "Conversation marked as read." }); + } catch (error) { + logger.log("conversation-mark-read-error", "ERROR", "api", null, { + conversationid, + imexshopid, + error + }); + res.status(500).json({ error: "Failed to mark conversation as read." }); + } }; - -// Inbound Sample -// { -// "SmsSid": "SM5205ea340e06437799d9345e7283457c", -// "SmsStatus": "queued", -// "MessageStatus": "queued", -// "To": "+16049992002", -// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1", -// "MessageSid": "SM5205ea340e06437799d9345e7283457c", -// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c", -// "From": "+16043301606", -// "ApiVersion": "2010-04-01" -// } From 250faa672fd2997f728869a29f31876d77f03ed1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 14:53:01 -0800 Subject: [PATCH 04/30] feature/IO-3000-messaging-sockets-migrations2 - Updated Polling Intervals is now socket based over FCMToken based Signed-off-by: Dave Richer --- .../chat-popup/chat-popup.component.jsx | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index b9a500166..b2625261d 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; import { useLazyQuery, useQuery } from "@apollo/client"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -13,18 +13,21 @@ import ChatConversationContainer from "../chat-conversation/chat-conversation.co import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import "./chat-popup.styles.scss"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation, chatVisible: selectChatVisible }); + const mapDispatchToProps = (dispatch) => ({ toggleChatVisible: () => dispatch(toggleChatVisible()) }); export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) { const { t } = useTranslation(); - const [pollInterval, setpollInterval] = useState(0); + const [pollInterval, setPollInterval] = useState(0); + const { socket } = useContext(SocketContext); const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { fetchPolicy: "network-only", @@ -39,16 +42,29 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh ...(pollInterval > 0 ? { pollInterval } : {}) }); - const fcmToken = sessionStorage.getItem("fcmtoken"); - - //TODO: Change to be a fallback incase sockets shit the bed useEffect(() => { - if (fcmToken) { - setpollInterval(0); - } else { - setpollInterval(90000); + const handleSocketStatus = () => { + if (socket?.connected) { + setPollInterval(15 * 60 * 1000); // 15 minutes + } else { + setPollInterval(60 * 1000); // 60 seconds + } + }; + + handleSocketStatus(); + + if (socket) { + socket.on("connect", handleSocketStatus); + socket.on("disconnect", handleSocketStatus); } - }, [fcmToken]); + + return () => { + if (socket) { + socket.off("connect", handleSocketStatus); + socket.off("disconnect", handleSocketStatus); + } + }; + }, [socket]); useEffect(() => { if (chatVisible) @@ -56,6 +72,8 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh variables: { offset: 0 } + }).catch((err) => { + console.error(`Error fetching conversations: ${(err, err.message || "")}`); }); }, [chatVisible, getConversations]); @@ -65,10 +83,12 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh variables: { offset: data.conversations.length } + }).catch((err) => { + console.error(`Error fetching more conversations: ${(err, err.message || "")}`); }); }, [data, fetchMore]); - const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; + const unreadCount = unreadData?.messages_aggregate?.aggregate?.count || 0; return ( From 06afd6da5bdff46aa63bad3307e5ea501164c7a3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 18:22:27 -0800 Subject: [PATCH 05/30] feature/IO-3000-messaging-sockets-migrations2 - - Conversation Labels Synced - Job Tagging Synced Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 97 +++++++------------ ...chat-conversation-title-tags.component.jsx | 16 ++- .../chat-conversation-title.component.jsx | 11 ++- .../chat-conversation.component.jsx | 10 +- .../chat-conversation.container.jsx | 1 + .../chat-label/chat-label.component.jsx | 14 ++- .../chat-tag-ro/chat-tag-ro.container.jsx | 32 +++++- client/src/utils/GraphQLClient.js | 17 ++++ server/web-sockets/redisSocketEvents.js | 19 +++- 9 files changed, 142 insertions(+), 75 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 339acd9a6..120c184dd 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -97,70 +97,45 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleConversationChanged = (data) => { - const { type, conversationId, jobId, label } = data; + const { conversationId, type, job_conversations, ...fields } = data; - switch (type) { - case "conversation-marked-read": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - messages_aggregate: () => ({ aggregate: { count: 0 } }) - } - }); + // Identify the conversation in the Apollo cache + const cacheId = client.cache.identify({ + __typename: "conversations", + id: conversationId + }); - // Optionally, refetch queries if needed - // client.refetchQueries({ - // include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS] - // }); - break; - case "tag-added": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - job_conversations(existingJobConversations = []) { - return [...existingJobConversations, { __ref: `jobs:${jobId}` }]; - } - } - }); - break; - - case "tag-removed": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - job_conversations(existingJobConversations = []) { - return existingJobConversations.filter((jobRef) => jobRef.__ref !== `jobs:${jobId}`); - } - } - }); - break; - - case "label-changed": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - label() { - return label; - } - } - }); - break; - - default: - console.warn(`Unhandled conversation change type: ${type}`); + if (!cacheId) { + console.error(`Could not find conversation with id: ${conversationId}`); + return; } + + client.cache.modify({ + id: cacheId, + fields: { + ...Object.fromEntries( + Object.entries(fields).map(([key, value]) => [ + key, + (cached) => (value !== undefined ? value : cached) // Update with new value or keep existing + ]) + ), + ...(type === "conversation-marked-read" && { + messages_aggregate: () => ({ + aggregate: { count: 0 } // Reset unread count + }) + }), + ...(type === "tag-added" && { + job_conversations: (existing = []) => { + // Merge existing job_conversations with new ones + return [...existing, ...job_conversations]; + } + }), + ...(type === "tag-removed" && { + job_conversations: (existing = [], { readField }) => + existing.filter((jobConversationRef) => readField("jobid", jobConversationRef) !== data.jobId) + }) + } + }); }; socket.on("new-message-summary", handleNewMessageSummary); diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index d851e6c92..944ade8b0 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -1,13 +1,15 @@ import { useMutation } from "@apollo/client"; import { Tag } from "antd"; -import React from "react"; +import React, { useContext } from "react"; import { Link } from "react-router-dom"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatConversationTitleTags({ jobConversations }) { +export default function ChatConversationTitleTags({ jobConversations, bodyshop }) { const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); + const { socket } = useContext(SocketContext); const handleRemoveTag = (jobId) => { const convId = jobConversations[0].conversationid; @@ -27,6 +29,16 @@ export default function ChatConversationTitleTags({ jobConversations }) { } }); } + }).then(() => { + if (socket) { + // Emit the `conversation-modified` event + socket.emit("conversation-modified", { + bodyshopId: bodyshop.id, + conversationId: convId, + type: "tag-removed", + jobId: jobId + }); + } }); logImEXEvent("messaging_remove_job_tag", { conversationId: convId, diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 41cf0b441..243f699a0 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -7,14 +7,17 @@ import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; -export default function ChatConversationTitle({ conversation }) { +export default function ChatConversationTitle({ conversation, bodyshop }) { return ( {conversation && conversation.phone_num} - + - - + + ); diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index d4a9695c0..32e52d5f2 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -6,7 +6,13 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import "./chat-conversation.styles.scss"; -export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) { +export default function ChatConversationComponent({ + subState, + conversation, + messages, + handleMarkConversationAsRead, + bodyshop +}) { const [loading, error] = subState; if (loading) return ; @@ -18,7 +24,7 @@ export default function ChatConversationComponent({ subState, conversation, mess onMouseDown={handleMarkConversationAsRead} onKeyDown={handleMarkConversationAsRead} > - +
diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 58107ad7c..0fb60aea4 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -74,6 +74,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { conversation={convoData ? convoData.conversations_by_pk : {}} messages={convoData ? convoData.conversations_by_pk.messages : []} handleMarkConversationAsRead={handleMarkConversationAsRead} + bodyshop={bodyshop} /> ); } diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx index 7157d02c8..e577bbf23 100644 --- a/client/src/components/chat-label/chat-label.component.jsx +++ b/client/src/components/chat-label/chat-label.component.jsx @@ -1,14 +1,16 @@ import { PlusOutlined } from "@ant-design/icons"; import { useMutation } from "@apollo/client"; import { Input, notification, Spin, Tag, Tooltip } from "antd"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatLabel({ conversation }) { +export default function ChatLabel({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(false); const [value, setValue] = useState(conversation.label); + const { socket } = useContext(SocketContext); const { t } = useTranslation(); const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL); @@ -26,6 +28,14 @@ export default function ChatLabel({ conversation }) { }) }); } else { + if (socket) { + socket.emit("conversation-modified", { + type: "label-updated", + conversationId: conversation.id, + bodyshopId: bodyshop.id, + label: value + }); + } setEditing(false); } } catch (error) { diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index f9b6fa5ad..6e01ea5ed 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -2,16 +2,18 @@ import { PlusOutlined } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client"; import { Tag } from "antd"; import _ from "lodash"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import ChatTagRo from "./chat-tag-ro.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatTagRoContainer({ conversation }) { +export default function ChatTagRoContainer({ conversation, bodyshop }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const { socket } = useContext(SocketContext); const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS); @@ -32,7 +34,31 @@ export default function ChatTagRoContainer({ conversation }) { const handleInsertTag = (value, option) => { logImEXEvent("messaging_add_job_tag"); - insertTag({ variables: { jobId: option.key } }); + + insertTag({ + variables: { jobId: option.key } + }).then(() => { + if (socket) { + // Find the job details from the search data + const selectedJob = data?.search_jobs.find((job) => job.id === option.key); + if (!selectedJob) return; + const newJobConversation = { + __typename: "job_conversations", + jobid: selectedJob.id, + conversationid: conversation.id, + job: { + __typename: "jobs", + ...selectedJob + } + }; + socket.emit("conversation-modified", { + conversationId: conversation.id, + bodyshopId: bodyshop.id, + type: "tag-added", + job_conversations: [newJobConversation] + }); + } + }); setOpen(false); }; diff --git a/client/src/utils/GraphQLClient.js b/client/src/utils/GraphQLClient.js index 789bfdf10..e0d409bd7 100644 --- a/client/src/utils/GraphQLClient.js +++ b/client/src/utils/GraphQLClient.js @@ -149,6 +149,23 @@ const cache = new InMemoryCache({ fields: { conversations: offsetLimitPagination() } + }, + conversations: { + fields: { + job_conversations: { + keyArgs: false, // Indicates that all job_conversations share the same key + merge(existing = [], incoming) { + // Merge existing and incoming job_conversations + const merged = [ + ...existing, + ...incoming.filter( + (incomingItem) => !existing.some((existingItem) => existingItem.__ref === incomingItem.__ref) + ) + ]; + return merged; + } + } + } } } }); diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 8e1358994..d2dd9eb7b 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -145,7 +145,6 @@ const redisSocketEvents = ({ error: error.message, stack: error.stack }); - socket.emit("error", { message: "Failed to join conversation" }); } }; const leaveConversationRoom = ({ bodyshopId, conversationId }) => { @@ -162,6 +161,24 @@ const redisSocketEvents = ({ } }; + const conversationModified = ({ bodyshopId, conversationId, ...fields }) => { + try { + // Retrieve the room name for the conversation + const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + // Emit the updated data to all clients in the room + io.to(room).emit("conversation-changed", { + conversationId, + ...fields + }); + } catch (error) { + logger.log("Failed to handle conversation modification", "error", "io-redis", null, { + error: error.message, + stack: error.stack + }); + } + }; + + socket.on("conversation-modified", conversationModified); socket.on("join-bodyshop-conversation", joinConversationRoom); socket.on("leave-bodyshop-conversation", leaveConversationRoom); }; From 15151cb4ac0bb943cda1125cbf51e44383632259 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 19:23:35 -0800 Subject: [PATCH 06/30] feature/IO-3000-messaging-sockets-migrations2 - - sync send - fix status events Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 78 +++++++++++++++---- .../chat-send-message.component.jsx | 17 +++- server/sms/status.js | 14 +--- server/web-sockets/redisSocketEvents.js | 13 ++++ 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 120c184dd..44b79c3b3 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -1,4 +1,5 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; +import { gql } from "@apollo/client"; export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; @@ -14,8 +15,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { const fullConversation = { ...newConversation, - phone_num: newConversation.phone_num, - id: newConversation.id, updated_at: newConversation.updated_at || new Date().toISOString(), unreadcnt: newConversation.unreadcnt || 0, archived: newConversation.archived || false, @@ -77,21 +76,43 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleMessageChanged = (message) => { - // Find the message in the cache and update all fields dynamically client.cache.modify({ - id: client.cache.identify({ - __typename: "messages", - id: message.id - }), + id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), fields: { - // Dynamically update all fields based on the incoming message object - __typename: (existingType) => existingType || "messages", // Ensure __typename is preserved - ...Object.fromEntries( - Object.entries(message).map(([key, value]) => [ - key, - (cached) => (value !== undefined ? value : cached) // Update with new value or keep existing - ]) - ) + ...(message.type === "status-changed" && { + messages(existing = [], { readField }) { + return existing.map((messageRef) => { + // Match the message by ID + if (readField("id", messageRef) === message.id) { + const currentStatus = readField("status", messageRef); + + // Prevent overwriting if the current status is already "delivered" + if (currentStatus === "delivered") { + return messageRef; + } + + // Update the existing message fields + return client.cache.writeFragment({ + id: messageRef.__ref, + fragment: gql` + fragment UpdatedMessage on messages { + id + status + conversationid + __typename + } + `, + data: { + __typename: "messages", + ...message // Only update the fields provided in the message object + } + }); + } + + return messageRef; // Keep other messages unchanged + }); + } + }) } }); }; @@ -113,6 +134,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { client.cache.modify({ id: cacheId, fields: { + // This is a catch-all for just sending it fields off conversation ...Object.fromEntries( Object.entries(fields).map(([key, value]) => [ key, @@ -138,6 +160,30 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); }; + const handleNewMessage = ({ conversationId, message }) => { + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + messages(existing = []) { + const newMessageRef = client.cache.writeFragment({ + data: message, + fragment: gql` + fragment NewMessage on messages { + id + body + createdAt + selectedMedia + imexshopid + } + ` + }); + return [...existing, newMessageRef]; + } + } + }); + }; + + socket.on("new-message", handleNewMessage); socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); @@ -146,9 +192,9 @@ export const registerMessagingHandlers = ({ socket, client }) => { export const unregisterMessagingHandlers = ({ socket }) => { if (!socket) return; + socket.off("new-message"); socket.off("new-message-summary"); socket.off("new-message-detailed"); socket.off("message-changed"); - socket.off("message-changed"); socket.off("conversation-changed"); }; diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index 918be1b5f..2ea90322e 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -1,6 +1,6 @@ import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; import { Input, Spin } from "antd"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -10,6 +10,7 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging. import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -25,6 +26,8 @@ const mapDispatchToProps = (dispatch) => ({ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) { const inputArea = useRef(null); const [selectedMedia, setSelectedMedia] = useState([]); + const { socket } = useContext(SocketContext); + useEffect(() => { inputArea.current.focus(); }, [isSending, setMessage]); @@ -37,14 +40,22 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi logImEXEvent("messaging_send_message"); if (selectedImages.length < 11) { - sendMessage({ + const newMessage = { to: conversation.phone_num, body: message || "", messagingServiceSid: bodyshop.messagingservicesid, conversationid: conversation.id, selectedMedia: selectedImages, imexshopid: bodyshop.imexshopid - }); + }; + sendMessage(newMessage); + if (socket) { + socket.emit("message-added", { + conversationId: conversation.id, + bodyshopId: bodyshop.id, + message: newMessage + }); + } setSelectedMedia( selectedMedia.map((i) => { return { ...i, isSelected: false }; diff --git a/server/sms/status.js b/server/sms/status.js index 9527ed5f8..bfdeb5ed2 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -36,7 +36,9 @@ exports.status = async (req, res) => { }); ioRedis.to(conversationRoom).emit("message-changed", { - message + ...message, + status: SmsStatus, + type: "status-changed" }); } else { logger.log("sms-status-update-warning", "WARN", "api", null, { @@ -80,21 +82,11 @@ exports.markConversationRead = async (req, res) => { const broadcastRoom = getBodyshopRoom(bodyshopid); - const conversationRoom = getBodyshopConversationRoom({ - bodyshopId: bodyshopid, - conversationId: conversationid - }); - ioRedis.to(broadcastRoom).emit("conversation-changed", { type: "conversation-marked-read", conversationId: conversationid }); - ioRedis.to(conversationRoom).emit("message-changed", { - type: "all-messages-marked-read", - conversationId: conversationid - }); - res.status(200).json({ success: true, message: "Conversation marked as read." }); } catch (error) { logger.log("conversation-mark-read-error", "ERROR", "api", null, { diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index d2dd9eb7b..33faab361 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -178,6 +178,19 @@ const redisSocketEvents = ({ } }; + const messageAdded = ({ bodyshopId, conversationId, message }) => { + try { + const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + io.to(room).emit("new-message", message); + } catch (error) { + logger.log("Failed to handle new message", "error", "io-redis", null, { + error: error.message, + stack: error.stack + }); + } + }; + + socket.on("message-added", messageAdded); socket.on("conversation-modified", conversationModified); socket.on("join-bodyshop-conversation", joinConversationRoom); socket.on("leave-bodyshop-conversation", leaveConversationRoom); From 5392659db6d8b846bbf7ee2c83bba589bfea9a79 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 11:32:43 -0800 Subject: [PATCH 07/30] feature/IO-3000-messaging-sockets-migrations2 - - Checkpoint Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 92 +++++++++++++++---- .../chat-archive-button.component.jsx | 20 +++- .../chat-conversation-title.component.jsx | 2 +- .../chat-conversation.container.jsx | 1 - .../chat-send-message.component.jsx | 3 +- server/sms/status.js | 6 ++ server/web-sockets/redisSocketEvents.js | 2 +- 7 files changed, 100 insertions(+), 26 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 44b79c3b3..ab4473e97 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -1,35 +1,45 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; import { gql } from "@apollo/client"; +const logLocal = (message, ...args) => { + if (import.meta.env.PROD) { + return; + } + console.log(`==================== ${message} ====================`); + console.dir({ ...args }); +}; + export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; const handleNewMessageSummary = (message) => { const { conversationId, newConversation, existingConversation, isoutbound } = message; + logLocal("handleNewMessageSummary", message); + if (!existingConversation && newConversation?.phone_num) { const queryResults = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }); - - const fullConversation = { - ...newConversation, - updated_at: newConversation.updated_at || new Date().toISOString(), - unreadcnt: newConversation.unreadcnt || 0, - archived: newConversation.archived || false, - label: newConversation.label || null, - job_conversations: newConversation.job_conversations || [], - messages_aggregate: newConversation.messages_aggregate || { - aggregate: { count: isoutbound ? 0 : 1 } - } - }; - client.cache.writeQuery({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 }, data: { - conversations: [fullConversation, ...(queryResults?.conversations || [])] + conversations: [ + { + ...newConversation, + updated_at: newConversation.updated_at || new Date().toISOString(), + unreadcnt: newConversation.unreadcnt || 0, + archived: newConversation.archived || false, + label: newConversation.label || null, + job_conversations: newConversation.job_conversations || [], + messages_aggregate: newConversation.messages_aggregate || { + aggregate: { count: isoutbound ? 0 : 1 } + } + }, + ...(queryResults?.conversations || []) + ] } }); } else { @@ -51,9 +61,12 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); } }; + const handleNewMessageDetailed = (message) => { const { conversationId, newMessage } = message; + logLocal("handleNewMessageDetailed", message); + // Append the new message to the conversation's message list const queryResults = client.cache.readQuery({ query: GET_CONVERSATION_DETAILS, @@ -76,6 +89,10 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleMessageChanged = (message) => { + if (!message) return; + + logLocal("handleMessageChanged", message); + client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), fields: { @@ -118,8 +135,12 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleConversationChanged = (data) => { + if (!data) return; + const { conversationId, type, job_conversations, ...fields } = data; + logLocal("handleConversationChanged", data); + // Identify the conversation in the Apollo cache const cacheId = client.cache.identify({ __typename: "conversations", @@ -161,23 +182,60 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleNewMessage = ({ conversationId, message }) => { + if (!conversationId || !message.id || !message.text) { + return; + } + + logLocal("handleNewMessage", { conversationId, message }); + client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { messages(existing = []) { + // Ensure that the `message` object matches the schema const newMessageRef = client.cache.writeFragment({ - data: message, + data: { + __typename: "messages", + id: message.id, + body: message.text, + selectedMedia: message.image_path || [], + imexshopid: message.userid, + status: message.status, + created_at: message.created_at, + read: message.read + }, fragment: gql` fragment NewMessage on messages { id body - createdAt selectedMedia imexshopid + status + created_at + read } ` }); - return [...existing, newMessageRef]; + + // Prevent duplicates by checking if the message already exists + const isDuplicate = existing.some( + (msgRef) => + client.cache.readFragment({ + id: msgRef.__ref, + fragment: gql` + fragment CheckMessage on messages { + id + } + ` + })?.id === message.id + ); + + // We already have it, so return the existing list + if (isDuplicate) { + return existing; + } + + return [...existing, newMessageRef]; // Add the new message reference } } }); diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx index 755e8f514..f7ed348c1 100644 --- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx +++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx @@ -1,21 +1,31 @@ import { useMutation } from "@apollo/client"; import { Button } from "antd"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatArchiveButton({ conversation }) { +export default function ChatArchiveButton({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); + const { socket } = useContext(SocketContext); + const handleToggleArchive = async () => { setLoading(true); - await updateConversation({ - variables: { id: conversation.id, archived: !conversation.archived }, - refetchQueries: ["CONVERSATION_LIST_QUERY"] + const updatedConversation = await updateConversation({ + variables: { id: conversation.id, archived: !conversation.archived } }); + if (socket) { + socket.emit("conversation-modified", { + conversationId: conversation.id, + bodyshopId: bodyshop.id, + archived: updatedConversation.data.update_conversations_by_pk.archived + }); + } + setLoading(false); }; diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 243f699a0..7754ea347 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -18,7 +18,7 @@ export default function ChatConversationTitle({ conversation, bodyshop }) { bodyshop={bodyshop} /> - + ); } diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 0fb60aea4..613d96288 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -26,7 +26,6 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); - const { socket } = useContext(SocketContext); useEffect(() => { diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index 2ea90322e..e86531ee4 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -50,10 +50,11 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi }; sendMessage(newMessage); if (socket) { + const lastMessage = conversation.messages?.[conversation.messages.length - 1]; // Get the last message socket.emit("message-added", { conversationId: conversation.id, bodyshopId: bodyshop.id, - message: newMessage + message: lastMessage }); } setSelectedMedia( diff --git a/server/sms/status.js b/server/sms/status.js index bfdeb5ed2..51d2f7ecb 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -15,6 +15,11 @@ exports.status = async (req, res) => { } = req; try { + // Ignore status 'queued' + if (SmsStatus === "queued") { + return res.status(200).json({ message: "Status 'queued' disregarded." }); + } + // Update message status in the database const response = await client.request(queries.UPDATE_MESSAGE_STATUS, { msid: SmsSid, @@ -47,6 +52,7 @@ exports.status = async (req, res) => { warning: "No message returned from the database update." }); } + res.sendStatus(200); } catch (error) { logger.log("sms-status-update-error", "ERROR", "api", null, { diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 33faab361..a8e753f2f 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -181,7 +181,7 @@ const redisSocketEvents = ({ const messageAdded = ({ bodyshopId, conversationId, message }) => { try { const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); - io.to(room).emit("new-message", message); + io.to(room).emit("new-message", { message, conversationId }); } catch (error) { logger.log("Failed to handle new message", "error", "io-redis", null, { error: error.message, From 12aec3e3a0ab5c8df53ca3c11dead878d63290ae Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 12:03:01 -0800 Subject: [PATCH 08/30] feature/IO-3000-messaging-sockets-migrations2 - - Fix Chat Icon logger error - Fix Socket Robustness - added additional wss status for error - Installed ant-design icons Signed-off-by: Dave Richer --- client/package-lock.json | 1 + client/package.json | 1 + .../chat-message-list.component.jsx | 52 +----- .../chat-messages-list/renderMessage.jsx | 42 +++++ .../wss-status-display.component.jsx | 29 +++- client/src/contexts/SocketIO/useSocket.js | 149 +++++++++++------- server/web-sockets/redisSocketEvents.js | 34 ++-- 7 files changed, 180 insertions(+), 128 deletions(-) create mode 100644 client/src/components/chat-messages-list/renderMessage.jsx diff --git a/client/package-lock.json b/client/package-lock.json index 93b492582..141c819b6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -85,6 +85,7 @@ "web-vitals": "^3.5.2" }, "devDependencies": { + "@ant-design/icons": "^5.5.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.24.7", "@dotenvx/dotenvx": "^1.14.1", diff --git a/client/package.json b/client/package.json index 38d148706..fdb2b6b34 100644 --- a/client/package.json +++ b/client/package.json @@ -132,6 +132,7 @@ "@rollup/rollup-linux-x64-gnu": "4.6.1" }, "devDependencies": { + "@ant-design/icons": "^5.5.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.24.7", "@dotenvx/dotenvx": "^1.14.1", diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index 076941d8c..81d3296fc 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -1,11 +1,6 @@ -import Icon from "@ant-design/icons"; -import { Tooltip } from "antd"; -import i18n from "i18next"; -import dayjs from "../../utils/day"; -import React, { useRef, useEffect } from "react"; -import { MdDone, MdDoneAll } from "react-icons/md"; +import React, { useEffect, useRef } from "react"; import { Virtuoso } from "react-virtuoso"; -import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { renderMessage } from "./renderMessage"; import "./chat-message-list.styles.scss"; export default function ChatMessageListComponent({ messages }) { @@ -21,7 +16,7 @@ export default function ChatMessageListComponent({ messages }) { }); } }, 100); // Delay of 100ms to allow rendering - return () => clearTimeout(timer); // Cleanup the timer on unmount + return () => clearTimeout(timer); }, [messages.length]); // Run only once on component mount // Scroll to the bottom after the new messages are rendered @@ -37,52 +32,13 @@ export default function ChatMessageListComponent({ messages }) { }, 50); // Slight delay to ensure layout recalculates } }, [messages]); // Triggered when new messages are added - //TODO: Does this one need to come into the render of the method? - const renderMessage = (index) => { - const message = messages[index]; - return ( -
-
- -
- {message.image_path && - message.image_path.map((i, idx) => ( -
- - Received - -
- ))} -
{message.text}
-
-
- {message.status && ( -
- -
- )} -
- {message.isoutbound && ( -
- {i18n.t("messaging.labels.sentby", { - by: message.userid, - time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a") - })} -
- )} -
- ); - }; return (
renderMessage(index)} + itemContent={(index) => renderMessage(messages, index)} // Pass `messages` to renderMessage followOutput="smooth" // Ensure smooth scrolling when new data is appended style={{ height: "100%", width: "100%" }} /> diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx new file mode 100644 index 000000000..e6982f72b --- /dev/null +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -0,0 +1,42 @@ +import Icon from "@ant-design/icons"; +import { Tooltip } from "antd"; +import i18n from "i18next"; +import dayjs from "../../utils/day"; +import { MdDone, MdDoneAll } from "react-icons/md"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; + +export const renderMessage = (messages, index) => { + const message = messages[index]; + return ( +
+
+ +
+ {message.image_path && + message.image_path.map((i, idx) => ( +
+ + Received + +
+ ))} +
{message.text}
+
+
+ {message.status && (message.status === "sent" || message.status === "delivered") && ( +
+ +
+ )} +
+ {message.isoutbound && ( +
+ {i18n.t("messaging.labels.sentby", { + by: message.userid, + time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a") + })} +
+ )} +
+ ); +}; diff --git a/client/src/components/wss-status-display/wss-status-display.component.jsx b/client/src/components/wss-status-display/wss-status-display.component.jsx index 40ff0e42a..6eb2c9223 100644 --- a/client/src/components/wss-status-display/wss-status-display.component.jsx +++ b/client/src/components/wss-status-display/wss-status-display.component.jsx @@ -1,18 +1,33 @@ import { connect } from "react-redux"; -import { GlobalOutlined } from "@ant-design/icons"; +import { GlobalOutlined, WarningOutlined } from "@ant-design/icons"; import { createStructuredSelector } from "reselect"; import React from "react"; import { selectWssStatus } from "../../redux/application/application.selectors"; + const mapStateToProps = createStructuredSelector({ - //currentUser: selectCurrentUser wssStatus: selectWssStatus }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); -export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay); + +const mapDispatchToProps = (dispatch) => ({}); export function WssStatusDisplay({ wssStatus }) { console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus); - return ; + + let icon; + let color; + + if (wssStatus === "connected") { + icon = ; + color = "green"; + } else if (wssStatus === "error") { + icon = ; + color = "red"; + } else { + icon = ; + color = "gray"; // Default for other statuses like "disconnected" + } + + return {icon}; } + +export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay); diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index 885602fe3..c13141ca7 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -3,75 +3,102 @@ import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; import { store } from "../../redux/store"; import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; -import { useDispatch } from "react-redux"; const useSocket = (bodyshop) => { const socketRef = useRef(null); const [clientId, setClientId] = useState(null); - const dispatch = useDispatch(); useEffect(() => { + const initializeSocket = async (token) => { + if (!bodyshop || !bodyshop.id) return; + + const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; + + const socketInstance = SocketIO(endpoint, { + path: "/wss", + withCredentials: true, + auth: { token }, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000 + }); + + socketRef.current = socketInstance; + + // Handle socket events + const handleBodyshopMessage = (message) => { + if (!message || !message.type) return; + + switch (message.type) { + case "alert-update": + store.dispatch(addAlerts(message.payload)); + break; + default: + break; + } + + if (!import.meta.env.DEV) return; + console.log(`Received message for bodyshop ${bodyshop.id}:`, message); + }; + + const handleConnect = () => { + socketInstance.emit("join-bodyshop-room", bodyshop.id); + setClientId(socketInstance.id); + store.dispatch(setWssStatus("connected")); + }; + + const handleReconnect = () => { + store.dispatch(setWssStatus("connected")); + }; + + const handleConnectionError = (err) => { + console.error("Socket connection error:", err); + + // Handle token expiration + if (err.message.includes("auth/id-token-expired")) { + console.warn("Token expired, refreshing..."); + auth.currentUser?.getIdToken(true).then((newToken) => { + socketInstance.auth = { token: newToken }; // Update socket auth + socketInstance.connect(); // Retry connection + }); + } else { + store.dispatch(setWssStatus("error")); + } + }; + + const handleDisconnect = (reason) => { + console.warn("Socket disconnected:", reason); + store.dispatch(setWssStatus("disconnected")); + + // Manually trigger reconnection if necessary + if (!socketInstance.connected && reason !== "io server disconnect") { + setTimeout(() => { + if (socketInstance.disconnected) { + console.log("Manually triggering reconnection..."); + socketInstance.connect(); + } + }, 2000); // Retry after 2 seconds + } + }; + + // Register event handlers + socketInstance.on("connect", handleConnect); + socketInstance.on("reconnect", handleReconnect); + socketInstance.on("connect_error", handleConnectionError); + socketInstance.on("disconnect", handleDisconnect); + socketInstance.on("bodyshop-message", handleBodyshopMessage); + }; + const unsubscribe = auth.onIdTokenChanged(async (user) => { if (user) { - const newToken = await user.getIdToken(); + const token = await user.getIdToken(); if (socketRef.current) { - // Send new token to server - socketRef.current.emit("update-token", newToken); - } else if (bodyshop && bodyshop.id) { - // Initialize the socket - const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; - - const socketInstance = SocketIO(endpoint, { - path: "/wss", - withCredentials: true, - auth: { token: newToken }, - reconnectionAttempts: Infinity, - reconnectionDelay: 2000, - reconnectionDelayMax: 10000 - }); - - socketRef.current = socketInstance; - - const handleBodyshopMessage = (message) => { - if (!message || !message?.type) return; - - switch (message.type) { - case "alert-update": - store.dispatch(addAlerts(message.payload)); - break; - default: - break; - } - - if (!import.meta.env.DEV) return; - console.log(`Received message for bodyshop ${bodyshop.id}:`, message); - }; - - const handleConnect = () => { - socketInstance.emit("join-bodyshop-room", bodyshop.id); - setClientId(socketInstance.id); - store.dispatch(setWssStatus("connected")); - }; - - const handleReconnect = (attempt) => { - store.dispatch(setWssStatus("connected")); - }; - - const handleConnectionError = (err) => { - console.error("Socket connection error:", err); - store.dispatch(setWssStatus("error")); - }; - - const handleDisconnect = () => { - store.dispatch(setWssStatus("disconnected")); - }; - - socketInstance.on("connect", handleConnect); - socketInstance.on("reconnect", handleReconnect); - socketInstance.on("connect_error", handleConnectionError); - socketInstance.on("disconnect", handleDisconnect); - socketInstance.on("bodyshop-message", handleBodyshopMessage); + // Update token if socket exists + socketRef.current.emit("update-token", token); + } else { + // Initialize socket if not already connected + initializeSocket(token); } } else { // User is not authenticated @@ -82,7 +109,7 @@ const useSocket = (bodyshop) => { } }); - // Clean up the listener on unmount + // Clean up on unmount return () => { unsubscribe(); if (socketRef.current) { @@ -90,7 +117,7 @@ const useSocket = (bodyshop) => { socketRef.current = null; } }; - }, [bodyshop, dispatch]); + }, [bodyshop]); return { socket: socketRef.current, clientId }; }; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index a8e753f2f..90893a505 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -46,18 +46,25 @@ const redisSocketEvents = ({ // Token Update Events const registerUpdateEvents = (socket) => { + let latestTokenTimestamp = 0; + const updateToken = async (newToken) => { + const currentTimestamp = Date.now(); + latestTokenTimestamp = currentTimestamp; + try { - // noinspection UnnecessaryLocalVariableJS + // Verify token with Firebase Admin SDK const user = await admin.auth().verifyIdToken(newToken, true); + + // Skip outdated token validations + if (currentTimestamp < latestTokenTimestamp) { + createLogEvent(socket, "warn", "Outdated token validation skipped."); + return; + } + socket.user = user; - // If We ever want to persist user Data across workers - // await setSessionData(socket.id, "user", user); - - // Uncomment for further testing - // createLogEvent(socket, "debug", "Token updated successfully"); - + createLogEvent(socket, "debug", `Token updated successfully for socket ID: ${socket.id}`); socket.emit("token-updated", { success: true }); } catch (error) { if (error.code === "auth/id-token-expired") { @@ -66,14 +73,17 @@ const redisSocketEvents = ({ success: false, error: "Stale token." }); - } else { - createLogEvent(socket, "error", `Token update failed: ${error.message}`); - socket.emit("token-updated", { success: false, error: error.message }); - // For any other errors, optionally disconnect the socket - socket.disconnect(); + return; // Avoid disconnecting for expired tokens } + + createLogEvent(socket, "error", `Token update failed for socket ID: ${socket.id}, Error: ${error.message}`); + socket.emit("token-updated", { success: false, error: error.message }); + + // Optionally disconnect for invalid tokens or other errors + socket.disconnect(); } }; + socket.on("update-token", updateToken); }; From e734da7adce82d17f4711e0eeffe9992c03b26ad Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 12:04:32 -0800 Subject: [PATCH 09/30] feature/IO-3000-messaging-sockets-migrations2 - - Fix Bug in import Signed-off-by: Dave Richer --- client/src/components/profile-shops/profile-shops.container.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/profile-shops/profile-shops.container.jsx b/client/src/components/profile-shops/profile-shops.container.jsx index 46fe59aa2..4c5150913 100644 --- a/client/src/components/profile-shops/profile-shops.container.jsx +++ b/client/src/components/profile-shops/profile-shops.container.jsx @@ -5,7 +5,7 @@ import { QUERY_ALL_ASSOCIATIONS, UPDATE_ACTIVE_ASSOCIATION } from "../../graphql import AlertComponent from "../alert/alert.component"; import ProfileShopsComponent from "./profile-shops.component"; import axios from "axios"; -import { getToken } from " firebase/messaging"; +import { getToken } from "firebase/messaging"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; From 1cdd905037d7e65bef5a7156367802de2da1fbd7 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 13:23:28 -0800 Subject: [PATCH 10/30] feature/IO-3000-messaging-sockets-migrations2 - - Checkpoint, archiving works, cannot unarchive yet Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 32 +++++++++++++++++++ .../chat-archive-button.component.jsx | 1 + .../chat-conversation.component.jsx | 1 + server/web-sockets/redisSocketEvents.js | 2 +- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index ab4473e97..325770541 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -152,6 +152,38 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } + if (type === "conversation-archived") { + // Remove all messages associated with this conversation + const messageRefs = client.cache.readFragment({ + id: cacheId, + fragment: gql` + fragment ConversationMessages on conversations { + messages { + id + } + } + ` + }); + + if (messageRefs?.messages) { + messageRefs.messages.forEach((message) => { + const messageCacheId = client.cache.identify({ + __typename: "messages", + id: message.id + }); + if (messageCacheId) { + client.cache.evict({ id: messageCacheId }); + } + }); + } + + // Evict the conversation itself + client.cache.evict({ id: cacheId }); + client.cache.gc(); // Trigger garbage collection to clean up unused entries + + return; + } + client.cache.modify({ id: cacheId, fields: { diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx index f7ed348c1..2b8bcd054 100644 --- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx +++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx @@ -20,6 +20,7 @@ export default function ChatArchiveButton({ conversation, bodyshop }) { if (socket) { socket.emit("conversation-modified", { + type: "conversation-archived", conversationId: conversation.id, bodyshopId: bodyshop.id, archived: updatedConversation.data.update_conversations_by_pk.archived diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index 32e52d5f2..4188fbabe 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -15,6 +15,7 @@ export default function ChatConversationComponent({ }) { const [loading, error] = subState; + if (conversation?.archived) return null; if (loading) return ; if (error) return ; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 90893a505..366461adc 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -174,7 +174,7 @@ const redisSocketEvents = ({ const conversationModified = ({ bodyshopId, conversationId, ...fields }) => { try { // Retrieve the room name for the conversation - const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + const room = getBodyshopRoom(bodyshopId); // Emit the updated data to all clients in the room io.to(room).emit("conversation-changed", { conversationId, From cd592b671c68370c1017ff7f415d031b643944b9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 15:05:52 -0800 Subject: [PATCH 11/30] feature/IO-3000-messaging-sockets-migrations2 - - Checkpoint, Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 10 +++++++++- client/src/graphql/conversations.queries.js | 18 +++++++++++++++++- server/sms/receive.js | 3 ++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 325770541..b50d02c8a 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -22,6 +22,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }); + client.cache.writeQuery({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 }, @@ -49,7 +50,14 @@ export const registerMessagingHandlers = ({ socket, client }) => { id: conversationId }), fields: { - updated_at: () => new Date(), + updated_at: () => new Date().toISOString(), + archived(cached) { + // Unarchive the conversation if it was previously marked as archived + if (cached) { + return false; + } + return cached; + }, messages_aggregate(cached) { // Increment unread count only if the message is inbound if (!isoutbound) { diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index e05a474ab..03bc0426b 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -90,9 +90,25 @@ export const CONVERSATION_ID_BY_PHONE = gql` query CONVERSATION_ID_BY_PHONE($phone: String!) { conversations(where: { phone_num: { _eq: $phone } }) { id + phone_num + archived + label + unreadcnt job_conversations { jobid - id + conversationid + job { + id + ownr_fn + ownr_ln + ownr_co_nm + ro_number + } + } + messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) { + aggregate { + count + } } } } diff --git a/server/sms/receive.js b/server/sms/receive.js index 3fb721eec..f7f41652e 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -54,7 +54,8 @@ exports.receive = async (req, res) => { newMessage.conversation = { data: { bodyshopid: bodyshop.id, - phone_num: phone(req.body.From).phoneNumber + phone_num: phone(req.body.From).phoneNumber, + archived: false } }; From d2e1b32557a57bbe6b62124bd9dce281eed8608a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 17:35:04 -0800 Subject: [PATCH 12/30] feature/IO-3000-messaging-sockets-migrations2 - - Checkpoint, Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 140 ++++++++++++++---- .../chat-new-conversation.component.jsx | 7 +- .../chat-open-button.component.jsx | 7 +- .../schedule-event.component.jsx | 8 +- .../job-payments/job-payments.component.jsx | 24 ++- .../jobs-detail-header-actions.component.jsx | 10 +- .../payments-generate-link.component.jsx | 10 +- client/src/firebase/firebase.utils.js | 1 - client/src/graphql/conversations.queries.js | 22 +++ client/src/redux/messaging/messaging.sagas.js | 116 +++++++++++++-- 10 files changed, 272 insertions(+), 73 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index b50d02c8a..e19c56a93 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -142,14 +142,47 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); }; - const handleConversationChanged = (data) => { + const handleConversationChanged = async (data) => { if (!data) return; const { conversationId, type, job_conversations, ...fields } = data; - logLocal("handleConversationChanged", data); - // Identify the conversation in the Apollo cache + const updatedAt = new Date().toISOString(); + + const updateConversationList = (newConversation) => { + try { + const existingList = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 } + }); + + const updatedList = existingList?.conversations + ? [ + newConversation, + ...existingList.conversations.filter( + (conv) => conv.id !== newConversation.id // Prevent duplicates + ) + ] + : [newConversation]; + + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 }, + data: { + conversations: updatedList + } + }); + } catch (error) { + console.error("Error updating conversation list in the cache:", error); + } + }; + + if (type === "conversation-created") { + updateConversationList({ ...fields, job_conversations, updated_at: updatedAt }); + return; + } + const cacheId = client.cache.identify({ __typename: "conversations", id: conversationId @@ -161,20 +194,20 @@ export const registerMessagingHandlers = ({ socket, client }) => { } if (type === "conversation-archived") { - // Remove all messages associated with this conversation - const messageRefs = client.cache.readFragment({ - id: cacheId, - fragment: gql` - fragment ConversationMessages on conversations { - messages { - id + try { + // Evict messages associated with the conversation + const messageRefs = client.cache.readFragment({ + id: cacheId, + fragment: gql` + fragment ConversationMessages on conversations { + messages { + id + } } - } - ` - }); + ` + }); - if (messageRefs?.messages) { - messageRefs.messages.forEach((message) => { + messageRefs?.messages?.forEach((message) => { const messageCacheId = client.cache.identify({ __typename: "messages", id: message.id @@ -183,39 +216,84 @@ export const registerMessagingHandlers = ({ socket, client }) => { client.cache.evict({ id: messageCacheId }); } }); + + // Evict the conversation itself + client.cache.evict({ id: cacheId }); + client.cache.gc(); // Trigger garbage collection + } catch (error) { + console.error("Error archiving conversation:", error); } - - // Evict the conversation itself - client.cache.evict({ id: cacheId }); - client.cache.gc(); // Trigger garbage collection to clean up unused entries - return; } + if (type === "conversation-unarchived") { + try { + // Fetch the conversation from the database if not already in the cache + const existingConversation = client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId } + }); + + if (!existingConversation) { + const { data: fetchedData } = await client.query({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId }, + fetchPolicy: "network-only" + }); + + if (fetchedData?.conversations_by_pk) { + const conversationData = fetchedData.conversations_by_pk; + + // Enrich conversation data + const enrichedConversation = { + ...conversationData, + messages_aggregate: { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: conversationData.messages.filter((message) => !message.read && !message.isoutbound).length + } + }, + updated_at: updatedAt + }; + + updateConversationList(enrichedConversation); + } + } + + // Mark the conversation as unarchived in the cache + client.cache.modify({ + id: cacheId, + fields: { + archived: () => false, + updated_at: () => updatedAt + } + }); + } catch (error) { + console.error("Error unarchiving conversation:", error); + } + return; + } + + // Handle other types of updates (e.g., marked read, tags added/removed) client.cache.modify({ id: cacheId, fields: { - // This is a catch-all for just sending it fields off conversation ...Object.fromEntries( - Object.entries(fields).map(([key, value]) => [ - key, - (cached) => (value !== undefined ? value : cached) // Update with new value or keep existing - ]) + Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)]) ), ...(type === "conversation-marked-read" && { messages_aggregate: () => ({ - aggregate: { count: 0 } // Reset unread count + __typename: "messages_aggregate", + aggregate: { __typename: "messages_aggregate_fields", count: 0 } }) }), ...(type === "tag-added" && { - job_conversations: (existing = []) => { - // Merge existing job_conversations with new ones - return [...existing, ...job_conversations]; - } + job_conversations: (existing = []) => [...existing, ...job_conversations] }), ...(type === "tag-removed" && { job_conversations: (existing = [], { readField }) => - existing.filter((jobConversationRef) => readField("jobid", jobConversationRef) !== data.jobId) + existing.filter((jobRef) => readField("jobid", jobRef) !== data.jobId) }) } }); diff --git a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx index b0b6054e4..e45ffb9b6 100644 --- a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx +++ b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx @@ -1,11 +1,12 @@ import { PlusCircleFilled } from "@ant-design/icons"; import { Button, Form, Popover } from "antd"; -import React from "react"; +import React, { useContext } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { openChatByPhone } from "../../redux/messaging/messaging.actions"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -17,8 +18,10 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatNewConversation({ openChatByPhone }) { const { t } = useTranslation(); const [form] = Form.useForm(); + const { socket } = useContext(SocketContext); + const handleFinish = (values) => { - openChatByPhone({ phone_num: values.phoneNumber }); + openChatByPhone({ phone_num: values.phoneNumber, socket }); form.resetFields(); }; diff --git a/client/src/components/chat-open-button/chat-open-button.component.jsx b/client/src/components/chat-open-button/chat-open-button.component.jsx index d2f0de528..dcd41c041 100644 --- a/client/src/components/chat-open-button/chat-open-button.component.jsx +++ b/client/src/components/chat-open-button/chat-open-button.component.jsx @@ -1,6 +1,6 @@ import { notification } from "antd"; import parsePhoneNumber from "libphonenumber-js"; -import React from "react"; +import React, { useContext } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { openChatByPhone } from "../../redux/messaging/messaging.actions"; @@ -9,6 +9,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { searchingForConversation } from "../../redux/messaging/messaging.selectors"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) { const { t } = useTranslation(); + const { socket } = useContext(SocketContext); + if (!phone) return <>; if (!bodyshop.messagingservicesid) return {phone}; @@ -33,7 +36,7 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobi const p = parsePhoneNumber(phone, "CA"); if (searchingForConversation) return; //This is to prevent finding the same thing twice. if (p && p.isValid()) { - openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid }); + openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid, socket }); } else { notification["error"]({ message: t("messaging.error.invalidphone") }); } diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index e7f70ff65..bdf5f1a85 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -3,7 +3,7 @@ import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, import parsePhoneNumber from "libphonenumber-js"; import dayjs from "../../utils/day"; import queryString from "query-string"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; @@ -24,6 +24,7 @@ import ScheduleEventNote from "./schedule-event.note.component"; import { useMutation } from "@apollo/client"; import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -49,6 +50,8 @@ export function ScheduleEventComponent({ const searchParams = queryString.parse(useLocation().search); const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); const [title, setTitle] = useState(event.title); + const { socket } = useContext(SocketContext); + const blockContent = ( ({ setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })), - setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })), - openChatByPhone: (phone) => dispatch(openChatByPhone(phone)), - setMessage: (text) => dispatch(setMessage(text)) + setCardPaymentContext: (context) => + dispatch( + setModalContext({ + context: context, + modal: "cardPayment" + }) + ) }); -export function JobPayments({ - job, - jobRO, - bodyshop, - setMessage, - openChatByPhone, - setPaymentContext, - setCardPaymentContext, - refetch -}) { +export function JobPayments({ job, jobRO, bodyshop, setPaymentContext, setCardPaymentContext, refetch }) { const { treatments: { ImEXPay } } = useSplitTreatments({ @@ -133,7 +127,7 @@ export function JobPayments({ } ]; - //Same as in RO guard. If changed, update in both. + //Same as in RO guard. If changed, update in both. const total = useMemo(() => { return ( job.payments && diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index c9c3b8fb7..1fdeeec61 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -4,7 +4,7 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd"; import axios from "axios"; import parsePhoneNumber from "libphonenumber-js"; -import React, { useMemo, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; @@ -30,6 +30,7 @@ import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -126,6 +127,7 @@ export function JobsDetailHeaderActions({ const [updateJob] = useMutation(UPDATE_JOB); const [voidJob] = useMutation(VOID_JOB); const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID); + const { socket } = useContext(SocketContext); const { treatments: { ImEXPay } @@ -299,7 +301,8 @@ export function JobsDetailHeaderActions({ if (p && p.isValid()) { openChatByPhone({ phone_num: p.formatInternational(), - jobid: job.id + jobid: job.id, + socket }); setMessage( `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}` @@ -342,7 +345,8 @@ export function JobsDetailHeaderActions({ if (p && p.isValid()) { openChatByPhone({ phone_num: p.formatInternational(), - jobid: job.id + jobid: job.id, + socket }); setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`); } else { diff --git a/client/src/components/payments-generate-link/payments-generate-link.component.jsx b/client/src/components/payments-generate-link/payments-generate-link.component.jsx index b0f4b26b9..f9d84d10f 100644 --- a/client/src/components/payments-generate-link/payments-generate-link.component.jsx +++ b/client/src/components/payments-generate-link/payments-generate-link.component.jsx @@ -3,13 +3,14 @@ import { Button, Form, message, Popover, Space } from "antd"; import axios from "axios"; import Dinero from "dinero.js"; import { parsePhoneNumber } from "libphonenumber-js"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -28,6 +29,7 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [paymentLink, setPaymentLink] = useState(null); + const { socket } = useContext(SocketContext); const handleFinish = async ({ amount }) => { setLoading(true); @@ -50,7 +52,8 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope if (p) { openChatByPhone({ phone_num: p.formatInternational(), - jobid: job.id + jobid: job.id, + socket }); setMessage( t("payments.labels.smspaymentreminder", { @@ -106,7 +109,8 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope const p = parsePhoneNumber(job.ownr_ph1, "CA"); openChatByPhone({ phone_num: p.formatInternational(), - jobid: job.id + jobid: job.id, + socket }); setMessage( t("payments.labels.smspaymentreminder", { diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js index 46a460525..33da2eeac 100644 --- a/client/src/firebase/firebase.utils.js +++ b/client/src/firebase/firebase.utils.js @@ -4,7 +4,6 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { store } from "../redux/store"; -import axios from "axios"; const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG); initializeApp(config); diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index 03bc0426b..373d70691 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -59,6 +59,8 @@ export const GET_CONVERSATION_DETAILS = gql` id phone_num archived + updated_at + unreadcnt label job_conversations { jobid @@ -119,6 +121,26 @@ export const CREATE_CONVERSATION = gql` insert_conversations(objects: $conversation) { returning { id + phone_num + archived + label + unreadcnt + job_conversations { + jobid + conversationid + job { + id + ownr_fn + ownr_ln + ownr_co_nm + ro_number + } + } + messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) { + aggregate { + count + } + } } } } diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index 86757a427..fa17c6db5 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -2,7 +2,13 @@ import axios from "axios"; import parsePhoneNumber from "libphonenumber-js"; import { all, call, put, select, takeLatest } from "redux-saga/effects"; import { logImEXEvent } from "../../firebase/firebase.utils"; -import { CONVERSATION_ID_BY_PHONE, CREATE_CONVERSATION } from "../../graphql/conversations.queries"; +import { + CONVERSATION_ID_BY_PHONE, + CONVERSATION_LIST_QUERY, + CREATE_CONVERSATION, + GET_CONVERSATION_DETAILS, + TOGGLE_CONVERSATION_ARCHIVE +} from "../../graphql/conversations.queries"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import client from "../../utils/GraphQLClient"; import { selectBodyshop } from "../user/user.selectors"; @@ -27,23 +33,24 @@ export function* onOpenChatByPhone() { export function* openChatByPhone({ payload }) { logImEXEvent("messaging_open_by_phone"); - const { phone_num, jobid } = payload; - + const { socket, phone_num, jobid } = payload; const p = parsePhoneNumber(phone_num, "CA"); const bodyshop = yield select(selectBodyshop); + try { const { data: { conversations } } = yield client.query({ query: CONVERSATION_ID_BY_PHONE, variables: { phone: p.number }, - fetchPolicy: 'no-cache' + fetchPolicy: "no-cache" }); if (conversations.length === 0) { + // No conversation exists, create a new one const { data: { - insert_conversations: { returning: newConversationsId } + insert_conversations: { returning: newConversations } } } = yield client.mutate({ mutation: CREATE_CONVERSATION, @@ -57,26 +64,107 @@ export function* openChatByPhone({ payload }) { ] } }); - yield put(setSelectedConversation(newConversationsId[0].id)); - } else if (conversations.length === 1) { - //got the ID. Open it. - yield put(setSelectedConversation(conversations[0].id)); - //Check to see if this job ID is already a child of it. If not add the tag. - if (jobid && !conversations[0].job_conversations.find((jc) => jc.jobid === jobid)) + const createdConversation = newConversations[0]; // Get the newly created conversation + + // Emit event for new conversation with full details + if (socket) { + socket.emit("conversation-modified", { + bodyshopId: bodyshop.id, + type: "conversation-created", + ...createdConversation + }); + } + + // Set the newly created conversation as selected + yield put(setSelectedConversation(createdConversation.id)); + } else if (conversations.length === 1) { + const conversation = conversations[0]; + + if (conversation.archived) { + // Conversation is archived, unarchive it in the DB + const { + data: { update_conversations_by_pk: updatedConversation } + } = yield client.mutate({ + mutation: TOGGLE_CONVERSATION_ARCHIVE, + variables: { + id: conversation.id, + archived: false + } + }); + + if (socket) { + socket.emit("conversation-modified", { + type: "conversation-unarchived", + conversationId: updatedConversation.id, + bodyshopId: bodyshop.id, + archived: false + }); + } + + // Update the conversation list in the cache + const existingConversations = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 } + }); + + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 }, + data: { + conversations: [ + { + ...conversation, + archived: false, + updated_at: new Date().toISOString() + }, + ...(existingConversations?.conversations || []) + ] + } + }); + } + + // Check if the conversation exists in the cache + const cacheId = client.cache.identify({ + __typename: "conversations", + id: conversation.id + }); + + if (!cacheId) { + // Fetch the conversation details from the database + const { data } = yield client.query({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId: conversation.id } + }); + + // Write fetched data to the cache + client.cache.writeQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId: conversation.id }, + data + }); + } + + // Open the conversation + yield put(setSelectedConversation(conversation.id)); + + // Check and add job tag if needed + if (jobid && !conversation.job_conversations.find((jc) => jc.jobid === jobid)) { yield client.mutate({ mutation: INSERT_CONVERSATION_TAG, variables: { - conversationId: conversations[0].id, + conversationId: conversation.id, jobId: jobid } }); + } } else { - console.log("ERROR: Multiple conversations found. "); + // Multiple conversations found + console.error("ERROR: Multiple conversations found."); yield put(setSelectedConversation(null)); } } catch (error) { - console.log("Error in sendMessage saga.", error); + console.error("Error in openChatByPhone saga.", error); } } From 8229e3593ca6afc6c43b5d960af520059e17e9d9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 17:39:05 -0800 Subject: [PATCH 13/30] feature/IO-3000-messaging-sockets-migrations2 - - remove unused query, Signed-off-by: Dave Richer --- client/src/graphql/conversations.queries.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index 373d70691..33009ffbe 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -163,22 +163,3 @@ export const UPDATE_CONVERSATION_LABEL = gql` } } `; - -export const GET_CONVERSATION_MESSAGES = gql` - query GET_CONVERSATION_MESSAGES($conversationId: uuid!) { - conversation: conversations_by_pk(id: $conversationId) { - id - phone_num - updated_at - label - } - messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) { - id - text - created_at - read - isoutbound - userid - } - } -`; From 38f13346e5efa2feee36e435dca4bb6e7d3f07ed Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 18:44:38 -0800 Subject: [PATCH 14/30] feature/IO-3000-messaging-sockets-migrations2 - - testing and edge cases Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 201 ++++++++++---- .../chat-conversation-list.component.jsx | 11 +- .../chat-popup/chat-popup.component.jsx | 16 +- server/graphql-client/queries.js | 62 +---- server/sms/receive.js | 251 ++++++++---------- 5 files changed, 270 insertions(+), 271 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index e19c56a93..49bce2ff8 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -12,61 +12,164 @@ const logLocal = (message, ...args) => { export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; - const handleNewMessageSummary = (message) => { + const handleNewMessageSummary = async (message) => { const { conversationId, newConversation, existingConversation, isoutbound } = message; - logLocal("handleNewMessageSummary", message); - if (!existingConversation && newConversation?.phone_num) { - const queryResults = client.cache.readQuery({ - query: CONVERSATION_LIST_QUERY, - variables: { offset: 0 } - }); + const queryVariables = { offset: 0 }; - client.cache.writeQuery({ - query: CONVERSATION_LIST_QUERY, - variables: { offset: 0 }, - data: { - conversations: [ - { - ...newConversation, - updated_at: newConversation.updated_at || new Date().toISOString(), - unreadcnt: newConversation.unreadcnt || 0, - archived: newConversation.archived || false, - label: newConversation.label || null, - job_conversations: newConversation.job_conversations || [], - messages_aggregate: newConversation.messages_aggregate || { - aggregate: { count: isoutbound ? 0 : 1 } - } - }, - ...(queryResults?.conversations || []) - ] - } - }); - } else { - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - updated_at: () => new Date().toISOString(), - archived(cached) { - // Unarchive the conversation if it was previously marked as archived - if (cached) { - return false; + // Handle new conversation + if (!existingConversation && newConversation?.phone_num) { + try { + const queryResults = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: queryVariables + }); + + const enrichedConversation = { + ...newConversation, + updated_at: newConversation.updated_at || new Date().toISOString(), + unreadcnt: newConversation.unreadcnt || 0, + archived: newConversation.archived || false, + label: newConversation.label || null, + job_conversations: newConversation.job_conversations || [], + messages_aggregate: newConversation.messages_aggregate || { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: isoutbound ? 0 : 1 } - return cached; }, - messages_aggregate(cached) { - // Increment unread count only if the message is inbound - if (!isoutbound) { - return { aggregate: { count: cached.aggregate.count + 1 } }; - } - return cached; + __typename: "conversations" + }; + + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: queryVariables, + data: { + conversations: [enrichedConversation, ...(queryResults?.conversations || [])] } + }); + } catch (error) { + console.error("Error updating cache for new conversation:", error); + } + return; + } + + // Handle existing conversation + if (existingConversation) { + let conversationDetails; + + // Fetch or read the conversation details + try { + conversationDetails = client.cache.readFragment({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fragment: gql` + fragment ExistingConversation on conversations { + id + phone_num + updated_at + archived + label + unreadcnt + job_conversations { + jobid + conversationid + } + messages_aggregate { + aggregate { + count + } + } + __typename + } + ` + }); + } catch (error) { + console.warn("Conversation not found in cache, querying server..."); + } + + if (!conversationDetails) { + try { + const { data } = await client.query({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId }, + fetchPolicy: "network-only" + }); + conversationDetails = data?.conversations_by_pk; + } catch (error) { + console.error("Failed to fetch conversation details from server:", error); + return; } - }); + } + + if (!conversationDetails) { + console.error("Unable to retrieve conversation details. Skipping cache update."); + return; + } + + try { + const queryResults = client.cache.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: queryVariables + }); + + const isAlreadyInCache = queryResults?.conversations.some((conv) => conv.id === conversationId); + + if (!isAlreadyInCache) { + const enrichedConversation = { + ...conversationDetails, + archived: false, + __typename: "conversations", + messages_aggregate: { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: + conversationDetails.messages?.filter( + (message) => !message.read && !message.isoutbound // Count unread, inbound messages + ).length || 0 + } + } + }; + + client.cache.writeQuery({ + query: CONVERSATION_LIST_QUERY, + variables: queryVariables, + data: { + conversations: [enrichedConversation, ...(queryResults?.conversations || [])] + } + }); + } + // Update existing conversation fields + client.cache.modify({ + id: client.cache.identify({ + __typename: "conversations", + id: conversationId + }), + fields: { + updated_at: () => new Date().toISOString(), + archived: () => false, + messages_aggregate(cached) { + if (!isoutbound) { + return { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: cached.aggregate.count + 1 + } + }; + } + return cached; + } + } + }); + } catch (error) { + console.error("Error updating cache for existing conversation:", error); + } } }; @@ -300,7 +403,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleNewMessage = ({ conversationId, message }) => { - if (!conversationId || !message.id || !message.text) { + if (!conversationId || !message?.id || !message?.text) { return; } @@ -359,9 +462,9 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); }; - socket.on("new-message", handleNewMessage); socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); + socket.on("new-message", handleNewMessage); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); }; diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index 38221085d..a8f95f59d 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -19,12 +19,7 @@ const mapDispatchToProps = (dispatch) => ({ setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId)) }); -function ChatConversationListComponent({ - conversationList, - selectedConversation, - setSelectedConversation, - loadMoreConversations -}) { +function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) { const renderConversation = (index) => { const item = conversationList[index]; const cardContentRight = {item.updated_at}; @@ -69,13 +64,15 @@ function ChatConversationListComponent({ ); }; + // TODO: Can go back into virtuoso for additional fetch + // endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom + return (
renderConversation(index)} style={{ height: "100%", width: "100%" }} - endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom />
); diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index b2625261d..e67929d11 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -77,17 +77,6 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh }); }, [chatVisible, getConversations]); - const loadMoreConversations = useCallback(() => { - if (data) - fetchMore({ - variables: { - offset: data.conversations.length - } - }).catch((err) => { - console.error(`Error fetching more conversations: ${(err, err.message || "")}`); - }); - }, [data, fetchMore]); - const unreadCount = unreadData?.messages_aggregate?.aggregate?.count || 0; return ( @@ -114,10 +103,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh {loading ? ( ) : ( - + )} {selectedConversation ? : null} diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 0a2b6a853..72224dfb6 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1515,7 +1515,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) { }`; //TODO:AIO The above query used to have parts order lines in it. Validate that this doesn't need it. -exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!) { +exports.QUERY_JOB_COSTING_DETAILS = ` +query QUERY_JOB_COSTING_DETAILS($id: uuid!) { jobs_by_pk(id: $id) { ro_number clm_total @@ -2566,68 +2567,9 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) { } `; -exports.GET_CONVERSATIONS = `query GET_CONVERSATIONS($bodyshopId: uuid!) { - conversations( - where: { bodyshopid: { _eq: $bodyshopId }, archived: { _eq: false } }, - order_by: { updated_at: desc }, - limit: 50 - ) { - phone_num - id - updated_at - unreadcnt - archived - label - messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) { - aggregate { - count - } - } - job_conversations { - job { - id - ro_number - ownr_fn - ownr_ln - ownr_co_nm - } - } - } -} -`; - exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: uuid!) { update_messages(where: { conversationid: { _eq: $conversationId } }, _set: { read: true }) { affected_rows } } `; - -exports.GET_CONVERSATION_DETAILS = ` - query GET_CONVERSATION_DETAILS($conversationId: uuid!) { - conversation: conversations_by_pk(id: $conversationId) { - id - phone_num - updated_at - label - job_conversations { - job { - id - ro_number - ownr_fn - ownr_ln - ownr_co_nm - } - } - } - messages: messages(where: { conversationid: { _eq: $conversationId } }, order_by: { created_at: asc }) { - id - text - created_at - read - isoutbound - userid - image_path - } - } -`; diff --git a/server/sms/receive.js b/server/sms/receive.js index f7f41652e..63c11a260 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -16,23 +16,21 @@ exports.receive = async (req, res) => { ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; - logger.log("sms-inbound", "DEBUG", "api", null, { + const loggerData = { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, image_path: generateMediaArray(req.body) - }); + }; + + logger.log("sms-inbound", "DEBUG", "api", null, loggerData); if (!req.body || !req.body.MessagingServiceSid || !req.body.SmsMessageSid) { logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), + ...loggerData, type: "malformed-request" }); - res.status(400).json({ success: false, error: "Malformed Request" }); - return; + return res.status(400).json({ success: false, error: "Malformed Request" }); } try { @@ -41,6 +39,14 @@ exports.receive = async (req, res) => { phone: phone(req.body.From).phoneNumber }); + if (!response.bodyshops[0]) { + return res.status(400).json({ success: false, error: "No matching bodyshop" }); + } + + const bodyshop = response.bodyshops[0]; + const isNewConversation = bodyshop.conversations.length === 0; + const isDuplicate = bodyshop.conversations.length > 1; + let newMessage = { msid: req.body.SmsMessageSid, text: req.body.Body, @@ -48,140 +54,105 @@ exports.receive = async (req, res) => { image_path: generateMediaArray(req.body) }; - if (response.bodyshops[0]) { - const bodyshop = response.bodyshops[0]; - if (bodyshop.conversations.length === 0) { - newMessage.conversation = { - data: { - bodyshopid: bodyshop.id, - phone_num: phone(req.body.From).phoneNumber, - archived: false - } - }; - - try { - const insertresp = await client.request(queries.RECEIVE_MESSAGE, { msg: newMessage }); - const createdConversation = insertresp?.insert_messages?.returning?.[0]?.conversation || null; - const message = insertresp?.insert_messages?.returning?.[0]; - - if (!createdConversation) { - throw new Error("Conversation data is missing from the response."); - } - - const broadcastRoom = getBodyshopRoom(createdConversation.bodyshop.id); - const conversationRoom = getBodyshopConversationRoom({ - bodyshopId: message.conversation.bodyshop.id, - conversationId: message.conversation.id - }); - - ioRedis.to(broadcastRoom).emit("new-message-summary", { - isoutbound: false, - existingConversation: false, - newConversation: createdConversation, - conversationId: createdConversation.id, - updated_at: message.updated_at, - msid: message.sid, - summary: true - }); - - ioRedis.to(conversationRoom).emit("new-message-detailed", { - newMessage: message, - isoutbound: false, - newConversation: createdConversation, - existingConversation: false, - conversationId: createdConversation.id, - summary: false - }); - - logger.log("sms-inbound-success", "DEBUG", "api", null, { - newMessage, - createdConversation - }); - - res.status(200).send(""); - return; - } catch (e) { - handleError(req, e, res, "RECEIVE_MESSAGE"); - return; - } - } else if (bodyshop.conversations.length === 1) { - newMessage.conversationid = bodyshop.conversations[0].id; - } else { - logger.log("sms-inbound-error", "ERROR", "api", null, { - msid: req.body.SmsMessageSid, - text: req.body.Body, - image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body), - messagingServiceSid: req.body.MessagingServiceSid, - type: "duplicate-phone" - }); - res.status(400).json({ success: false, error: "Duplicate phone number" }); - return; - } - - try { - const insertresp = await client.request(queries.INSERT_MESSAGE, { - msg: newMessage, - conversationid: newMessage.conversationid - }); - - const message = insertresp.insert_messages.returning[0]; - const data = { - type: "messaging-inbound", - conversationid: message.conversationid || "", - text: message.text || "", - messageid: message.id || "", - phone_num: message.conversation.phone_num || "" - }; - - const fcmresp = await admin.messaging().send({ - topic: `${message.conversation.bodyshop.imexshopid}-messaging`, - notification: { - title: InstanceManager({ - imex: `ImEX Online Message - ${data.phone_num}`, - rome: `Rome Online Message - ${data.phone_num}`, - promanager: `ProManager Message - ${data.phone_num}` - }), - body: message.image_path ? `Image ${message.text}` : message.text - }, - data - }); - - logger.log("sms-inbound-success", "DEBUG", "api", null, { - newMessage, - fcmresp - }); - - const broadcastRoom = getBodyshopRoom(message.conversation.bodyshop.id); - const conversationRoom = getBodyshopConversationRoom({ - bodyshopId: message.conversation.bodyshop.id, - conversationId: message.conversation.id - }); - - ioRedis.to(broadcastRoom).emit("new-message-summary", { - isoutbound: false, - existingConversation: true, - conversationId: message.conversationid, - updated_at: message.updated_at, - msid: message.sid, - summary: true - }); - - ioRedis.to(conversationRoom).emit("new-message-detailed", { - newMessage: message, - isoutbound: false, - existingConversation: true, - conversationId: message.conversationid, - summary: false - }); - - res.status(200).send(""); - } catch (e) { - handleError(req, e, res, "INSERT_MESSAGE"); - } + if (isDuplicate) { + logger.log("sms-inbound-error", "ERROR", "api", null, { + ...loggerData, + messagingServiceSid: req.body.MessagingServiceSid, + type: "duplicate-phone" + }); + return res.status(400).json({ success: false, error: "Duplicate phone number" }); } + + if (isNewConversation) { + newMessage.conversation = { + data: { + bodyshopid: bodyshop.id, + phone_num: phone(req.body.From).phoneNumber, + archived: false + } + }; + } else { + const existingConversation = bodyshop.conversations[0]; + + // Update the conversation to unarchive it + if (existingConversation.archived) { + await client.request(queries.UNARCHIVE_CONVERSATION, { + id: existingConversation.id, + archived: false + }); + } + + newMessage.conversationid = existingConversation.id; + } + + const query = isNewConversation ? queries.RECEIVE_MESSAGE : queries.INSERT_MESSAGE; + const variables = isNewConversation + ? { msg: newMessage } + : { msg: newMessage, conversationid: newMessage.conversationid }; + + const insertresp = await client.request(query, variables); + const message = insertresp?.insert_messages?.returning?.[0]; + const conversation = message?.conversation || null; + + if (!conversation) { + throw new Error("Conversation data is missing from the response."); + } + + const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); + const conversationRoom = getBodyshopConversationRoom({ + bodyshopId: conversation.bodyshop.id, + conversationId: conversation.id + }); + + const commonPayload = { + isoutbound: false, + conversationId: conversation.id, + updated_at: message.updated_at, + msid: message.sid + }; + + ioRedis.to(broadcastRoom).emit("new-message-summary", { + ...commonPayload, + existingConversation: !isNewConversation, + newConversation: isNewConversation ? conversation : null, + summary: true + }); + + ioRedis.to(conversationRoom).emit("new-message-detailed", { + newMessage: message, + ...commonPayload, + newConversation: isNewConversation ? conversation : null, + existingConversation: !isNewConversation, + summary: false + }); + + const fcmresp = await admin.messaging().send({ + topic: `${message.conversation.bodyshop.imexshopid}-messaging`, + notification: { + title: InstanceManager({ + imex: `ImEX Online Message - ${message.conversation.phone_num}`, + rome: `Rome Online Message - ${message.conversation.phone_num}`, + promanager: `ProManager Message - ${message.conversation.phone_num}` + }), + body: message.image_path ? `Image ${message.text}` : message.text + }, + data: { + type: "messaging-inbound", + conversationid: message.conversationid || "", + text: message.text || "", + messageid: message.id || "", + phone_num: message.conversation.phone_num || "" + } + }); + + logger.log("sms-inbound-success", "DEBUG", "api", null, { + newMessage, + fcmresp + }); + + res.status(200).send(""); } catch (e) { - handleError(req, e, res, "FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID"); + handleError(req, e, res, "RECEIVE_MESSAGE"); } }; From 525f795ce0d500620db09b1d266d16ec65f804e4 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 19:17:42 -0800 Subject: [PATCH 15/30] feature/IO-3000-messaging-sockets-migrations2 - - dumb down archive/unarchive Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 84 ++----------------- client/src/redux/messaging/messaging.sagas.js | 3 +- 2 files changed, 11 insertions(+), 76 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 49bce2ff8..8034353c8 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -250,6 +250,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { const { conversationId, type, job_conversations, ...fields } = data; logLocal("handleConversationChanged", data); + console.log(`--------------${type}-----------------`); const updatedAt = new Date().toISOString(); @@ -296,85 +297,18 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } - if (type === "conversation-archived") { + if (type === "conversation-unarchived" || type === "conversation-archived") { try { - // Evict messages associated with the conversation - const messageRefs = client.cache.readFragment({ - id: cacheId, - fragment: gql` - fragment ConversationMessages on conversations { - messages { - id - } - } - ` - }); - - messageRefs?.messages?.forEach((message) => { - const messageCacheId = client.cache.identify({ - __typename: "messages", - id: message.id - }); - if (messageCacheId) { - client.cache.evict({ id: messageCacheId }); - } - }); - - // Evict the conversation itself - client.cache.evict({ id: cacheId }); - client.cache.gc(); // Trigger garbage collection - } catch (error) { - console.error("Error archiving conversation:", error); - } - return; - } - - if (type === "conversation-unarchived") { - try { - // Fetch the conversation from the database if not already in the cache - const existingConversation = client.cache.readQuery({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId } - }); - - if (!existingConversation) { - const { data: fetchedData } = await client.query({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId }, - fetchPolicy: "network-only" - }); - - if (fetchedData?.conversations_by_pk) { - const conversationData = fetchedData.conversations_by_pk; - - // Enrich conversation data - const enrichedConversation = { - ...conversationData, - messages_aggregate: { - __typename: "messages_aggregate", - aggregate: { - __typename: "messages_aggregate_fields", - count: conversationData.messages.filter((message) => !message.read && !message.isoutbound).length - } - }, - updated_at: updatedAt - }; - - updateConversationList(enrichedConversation); - } - } - - // Mark the conversation as unarchived in the cache - client.cache.modify({ - id: cacheId, - fields: { - archived: () => false, - updated_at: () => updatedAt - } + // Refetch the conversation list query to update the UI + const queryVariables = { offset: 0 }; + await client.refetchQueries({ + include: [CONVERSATION_LIST_QUERY], + variables: queryVariables }); } catch (error) { - console.error("Error unarchiving conversation:", error); + console.error("Error refetching conversation list after unarchiving:", error); } + return; } diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index fa17c6db5..2ee7a8bac 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -34,6 +34,7 @@ export function* onOpenChatByPhone() { export function* openChatByPhone({ payload }) { logImEXEvent("messaging_open_by_phone"); const { socket, phone_num, jobid } = payload; + if (!socket || !phone_num) return; const p = parsePhoneNumber(phone_num, "CA"); const bodyshop = yield select(selectBodyshop); @@ -67,7 +68,7 @@ export function* openChatByPhone({ payload }) { const createdConversation = newConversations[0]; // Get the newly created conversation - // Emit event for new conversation with full details + // // Emit event for new conversation with full details if (socket) { socket.emit("conversation-modified", { bodyshopId: bodyshop.id, From 12ed8d3830534aed4cdb946d5b9e0997d689ae8b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 19:21:51 -0800 Subject: [PATCH 16/30] feature/IO-3000-messaging-sockets-migrations2 - - dumb down archive/unarchive Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 8034353c8..a20781dec 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -250,7 +250,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { const { conversationId, type, job_conversations, ...fields } = data; logLocal("handleConversationChanged", data); - console.log(`--------------${type}-----------------`); const updatedAt = new Date().toISOString(); @@ -287,6 +286,23 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } + if (type === "conversation-unarchived" || type === "conversation-archived") { + try { + // Refetch the conversation list and details queries + await client.refetchQueries({ + include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], + variables: [ + { query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }, + { query: GET_CONVERSATION_DETAILS, variables: { conversationId } } + ] + }); + } catch (error) { + console.error("Error refetching queries after conversation state change:", error); + } + + return; + } + const cacheId = client.cache.identify({ __typename: "conversations", id: conversationId @@ -297,21 +313,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } - if (type === "conversation-unarchived" || type === "conversation-archived") { - try { - // Refetch the conversation list query to update the UI - const queryVariables = { offset: 0 }; - await client.refetchQueries({ - include: [CONVERSATION_LIST_QUERY], - variables: queryVariables - }); - } catch (error) { - console.error("Error refetching conversation list after unarchiving:", error); - } - - return; - } - // Handle other types of updates (e.g., marked read, tags added/removed) client.cache.modify({ id: cacheId, From 141deff41eb19dc2016b0be7065811d1fc228e41 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 19:37:25 -0800 Subject: [PATCH 17/30] feature/IO-3000-messaging-sockets-migrations2 - - harden openMessageByPhone Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 38 +++++---- client/src/redux/messaging/messaging.sagas.js | 78 +++++-------------- 2 files changed, 39 insertions(+), 77 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index a20781dec..76cc2c741 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -286,23 +286,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } - if (type === "conversation-unarchived" || type === "conversation-archived") { - try { - // Refetch the conversation list and details queries - await client.refetchQueries({ - include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], - variables: [ - { query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }, - { query: GET_CONVERSATION_DETAILS, variables: { conversationId } } - ] - }); - } catch (error) { - console.error("Error refetching queries after conversation state change:", error); - } - - return; - } - const cacheId = client.cache.identify({ __typename: "conversations", id: conversationId @@ -313,6 +296,27 @@ export const registerMessagingHandlers = ({ socket, client }) => { return; } + if (type === "conversation-unarchived" || type === "conversation-archived") { + try { + const listQueryVariables = { offset: 0 }; + const detailsQueryVariables = { conversationId }; + + // Refetch the conversation list and details queries + await client.refetchQueries({ + include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], + variables: [ + { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, + { query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables } + ] + }); + + console.log("Refetched conversation list and details after state change."); + } catch (error) { + console.error("Error refetching queries after conversation state change:", error); + } + + return; + } // Handle other types of updates (e.g., marked read, tags added/removed) client.cache.modify({ id: cacheId, diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index 2ee7a8bac..d2b428e1f 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -68,24 +68,23 @@ export function* openChatByPhone({ payload }) { const createdConversation = newConversations[0]; // Get the newly created conversation - // // Emit event for new conversation with full details - if (socket) { - socket.emit("conversation-modified", { - bodyshopId: bodyshop.id, - type: "conversation-created", - ...createdConversation - }); - } + // Emit event for new conversation with full details + socket.emit("conversation-modified", { + bodyshopId: bodyshop.id, + type: "conversation-created", + ...createdConversation + }); // Set the newly created conversation as selected yield put(setSelectedConversation(createdConversation.id)); } else if (conversations.length === 1) { const conversation = conversations[0]; + let updatedConversation = conversation; if (conversation.archived) { // Conversation is archived, unarchive it in the DB const { - data: { update_conversations_by_pk: updatedConversation } + data: { update_conversations_by_pk: unarchivedConversation } } = yield client.mutate({ mutation: TOGGLE_CONVERSATION_ARCHIVE, variables: { @@ -94,67 +93,26 @@ export function* openChatByPhone({ payload }) { } }); - if (socket) { - socket.emit("conversation-modified", { - type: "conversation-unarchived", - conversationId: updatedConversation.id, - bodyshopId: bodyshop.id, - archived: false - }); - } + updatedConversation = unarchivedConversation; - // Update the conversation list in the cache - const existingConversations = client.cache.readQuery({ - query: CONVERSATION_LIST_QUERY, - variables: { offset: 0 } - }); - - client.cache.writeQuery({ - query: CONVERSATION_LIST_QUERY, - variables: { offset: 0 }, - data: { - conversations: [ - { - ...conversation, - archived: false, - updated_at: new Date().toISOString() - }, - ...(existingConversations?.conversations || []) - ] - } - }); - } - - // Check if the conversation exists in the cache - const cacheId = client.cache.identify({ - __typename: "conversations", - id: conversation.id - }); - - if (!cacheId) { - // Fetch the conversation details from the database - const { data } = yield client.query({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId: conversation.id } - }); - - // Write fetched data to the cache - client.cache.writeQuery({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId: conversation.id }, - data + // Emit the unarchived event only once + socket.emit("conversation-modified", { + type: "conversation-unarchived", + conversationId: unarchivedConversation.id, + bodyshopId: bodyshop.id, + archived: false }); } // Open the conversation - yield put(setSelectedConversation(conversation.id)); + yield put(setSelectedConversation(updatedConversation.id)); // Check and add job tag if needed - if (jobid && !conversation.job_conversations.find((jc) => jc.jobid === jobid)) { + if (jobid && !updatedConversation.job_conversations.find((jc) => jc.jobid === jobid)) { yield client.mutate({ mutation: INSERT_CONVERSATION_TAG, variables: { - conversationId: conversation.id, + conversationId: updatedConversation.id, jobId: jobid } }); From 6504b27eca8d5d52193e2796fa16fa56aaa57831 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 21 Nov 2024 22:14:39 -0800 Subject: [PATCH 18/30] feature/IO-3000-messaging-sockets-migrations2 - - A lot of a lot of testing.... Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 453 ++++++++++-------- .../chat-conversation.container.jsx | 114 ++++- client/src/utils/GraphQLClient.js | 16 + server/graphql-client/queries.js | 3 + server/sms/status.js | 39 +- 5 files changed, 376 insertions(+), 249 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 76cc2c741..5b4f34ea1 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -14,40 +14,46 @@ export const registerMessagingHandlers = ({ socket, client }) => { const handleNewMessageSummary = async (message) => { const { conversationId, newConversation, existingConversation, isoutbound } = message; - logLocal("handleNewMessageSummary", message); + logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation }); const queryVariables = { offset: 0 }; + // Utility function to enrich conversation data + const enrichConversation = (conversation, isOutbound) => ({ + ...conversation, + updated_at: conversation.updated_at || new Date().toISOString(), + unreadcnt: conversation.unreadcnt || 0, + archived: conversation.archived || false, + label: conversation.label || null, + job_conversations: conversation.job_conversations || [], + messages_aggregate: conversation.messages_aggregate || { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: isOutbound ? 0 : 1 + } + }, + __typename: "conversations" + }); + // Handle new conversation if (!existingConversation && newConversation?.phone_num) { + logLocal("handleNewMessageSummary - New Conversation", newConversation); + try { const queryResults = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: queryVariables }); - const enrichedConversation = { - ...newConversation, - updated_at: newConversation.updated_at || new Date().toISOString(), - unreadcnt: newConversation.unreadcnt || 0, - archived: newConversation.archived || false, - label: newConversation.label || null, - job_conversations: newConversation.job_conversations || [], - messages_aggregate: newConversation.messages_aggregate || { - __typename: "messages_aggregate", - aggregate: { - __typename: "messages_aggregate_fields", - count: isoutbound ? 0 : 1 - } - }, - __typename: "conversations" - }; + const enrichedConversation = enrichConversation(newConversation, isoutbound); - client.cache.writeQuery({ - query: CONVERSATION_LIST_QUERY, - variables: queryVariables, - data: { - conversations: [enrichedConversation, ...(queryResults?.conversations || [])] + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + conversations(existingConversations = []) { + return [enrichedConversation, ...existingConversations]; + } } }); } catch (error) { @@ -60,13 +66,10 @@ export const registerMessagingHandlers = ({ socket, client }) => { if (existingConversation) { let conversationDetails; - // Fetch or read the conversation details + // Attempt to read existing conversation details from cache try { conversationDetails = client.cache.readFragment({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), + id: client.cache.identify({ __typename: "conversations", id: conversationId }), fragment: gql` fragment ExistingConversation on conversations { id @@ -89,9 +92,10 @@ export const registerMessagingHandlers = ({ socket, client }) => { ` }); } catch (error) { - console.warn("Conversation not found in cache, querying server..."); + logLocal("handleNewMessageSummary - Cache miss for conversation, fetching from server", { conversationId }); } + // Fetch conversation details from server if not in cache if (!conversationDetails) { try { const { data } = await client.query({ @@ -106,12 +110,14 @@ export const registerMessagingHandlers = ({ socket, client }) => { } } + // Validate that conversation details were retrieved if (!conversationDetails) { console.error("Unable to retrieve conversation details. Skipping cache update."); return; } try { + // Check if the conversation is already in the cache const queryResults = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: queryVariables @@ -120,46 +126,32 @@ export const registerMessagingHandlers = ({ socket, client }) => { const isAlreadyInCache = queryResults?.conversations.some((conv) => conv.id === conversationId); if (!isAlreadyInCache) { - const enrichedConversation = { - ...conversationDetails, - archived: false, - __typename: "conversations", - messages_aggregate: { - __typename: "messages_aggregate", - aggregate: { - __typename: "messages_aggregate_fields", - count: - conversationDetails.messages?.filter( - (message) => !message.read && !message.isoutbound // Count unread, inbound messages - ).length || 0 - } - } - }; + const enrichedConversation = enrichConversation(conversationDetails, isoutbound); - client.cache.writeQuery({ - query: CONVERSATION_LIST_QUERY, - variables: queryVariables, - data: { - conversations: [enrichedConversation, ...(queryResults?.conversations || [])] + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + conversations(existingConversations = []) { + return [enrichedConversation, ...existingConversations]; + } } }); } - // Update existing conversation fields + + // Update fields for the existing conversation in the cache client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), + id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { updated_at: () => new Date().toISOString(), archived: () => false, - messages_aggregate(cached) { + messages_aggregate(cached = { aggregate: { count: 0 } }) { + const currentCount = cached.aggregate?.count || 0; if (!isoutbound) { return { __typename: "messages_aggregate", aggregate: { __typename: "messages_aggregate_fields", - count: cached.aggregate.count + 1 + count: currentCount + 1 } }; } @@ -176,80 +168,107 @@ export const registerMessagingHandlers = ({ socket, client }) => { const handleNewMessageDetailed = (message) => { const { conversationId, newMessage } = message; - logLocal("handleNewMessageDetailed", message); + logLocal("handleNewMessageDetailed - Start", message); - // Append the new message to the conversation's message list - const queryResults = client.cache.readQuery({ - query: GET_CONVERSATION_DETAILS, - variables: { conversationId } - }); - - if (queryResults) { - client.cache.writeQuery({ + try { + // Check if the conversation exists in the cache + const queryResults = client.cache.readQuery({ query: GET_CONVERSATION_DETAILS, - variables: { conversationId }, - data: { - ...queryResults, - conversations_by_pk: { - ...queryResults.conversations_by_pk, - messages: [...queryResults.conversations_by_pk.messages, newMessage] + variables: { conversationId } + }); + + if (!queryResults?.conversations_by_pk) { + console.warn("Conversation not found in cache:", { conversationId }); + return; + } + + // Append the new message to the conversation's message list using cache.modify + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + messages(existingMessages = []) { + return [...existingMessages, newMessage]; } } }); + + logLocal("handleNewMessageDetailed - Message appended successfully", { conversationId, newMessage }); + } catch (error) { + console.error("Error updating conversation messages in cache:", error); } }; const handleMessageChanged = (message) => { - if (!message) return; + if (!message) { + logLocal("handleMessageChanged - No message provided", message); + return; + } - logLocal("handleMessageChanged", message); + logLocal("handleMessageChanged - Start", message); - client.cache.modify({ - id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), - fields: { - ...(message.type === "status-changed" && { - messages(existing = [], { readField }) { - return existing.map((messageRef) => { - // Match the message by ID + try { + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), + fields: { + messages(existingMessages = [], { readField }) { + return existingMessages.map((messageRef) => { + // Check if this is the message to update if (readField("id", messageRef) === message.id) { const currentStatus = readField("status", messageRef); - // Prevent overwriting if the current status is already "delivered" - if (currentStatus === "delivered") { - return messageRef; - } - - // Update the existing message fields - return client.cache.writeFragment({ - id: messageRef.__ref, - fragment: gql` - fragment UpdatedMessage on messages { - id - status - conversationid - __typename + // Handle known types of message changes + switch (message.type) { + case "status-changed": + // Prevent overwriting if the current status is already "delivered" + if (currentStatus === "delivered") { + logLocal("handleMessageChanged - Status already delivered, skipping update", { + messageId: message.id + }); + return messageRef; } - `, - data: { - __typename: "messages", - ...message // Only update the fields provided in the message object - } - }); + + // Update the status field + return { + ...messageRef, + status: message.status + }; + + case "text-updated": + // Handle changes to the message text + return { + ...messageRef, + text: message.text + }; + + // Add cases for other known message types as needed + + default: + // Log a warning for unhandled message types + logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); + return messageRef; + } } return messageRef; // Keep other messages unchanged }); } - }) - } - }); + } + }); + + logLocal("handleMessageChanged - Message updated successfully", { messageId: message.id, type: message.type }); + } catch (error) { + console.error("handleMessageChanged - Error modifying cache:", error); + } }; const handleConversationChanged = async (data) => { - if (!data) return; + if (!data) { + logLocal("handleConversationChanged - No data provided", data); + return; + } - const { conversationId, type, job_conversations, ...fields } = data; - logLocal("handleConversationChanged", data); + const { conversationId, type, job_conversations, messageIds, ...fields } = data; + logLocal("handleConversationChanged - Start", data); const updatedAt = new Date().toISOString(); @@ -263,9 +282,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { const updatedList = existingList?.conversations ? [ newConversation, - ...existingList.conversations.filter( - (conv) => conv.id !== newConversation.id // Prevent duplicates - ) + ...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates ] : [newConversation]; @@ -276,129 +293,149 @@ export const registerMessagingHandlers = ({ socket, client }) => { conversations: updatedList } }); + + logLocal("handleConversationChanged - Conversation list updated successfully", newConversation); } catch (error) { console.error("Error updating conversation list in the cache:", error); } }; - if (type === "conversation-created") { - updateConversationList({ ...fields, job_conversations, updated_at: updatedAt }); - return; - } + // Handle specific types + try { + switch (type) { + case "conversation-marked-read": + if (conversationId && messageIds?.length > 0) { + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + messages(existingMessages = [], { readField }) { + return existingMessages.map((message) => { + if (messageIds.includes(readField("id", message))) { + return { ...message, read: true }; + } + return message; + }); + }, + messages_aggregate: () => ({ + __typename: "messages_aggregate", + aggregate: { __typename: "messages_aggregate_fields", count: 0 } + }) + } + }); + } + break; - const cacheId = client.cache.identify({ - __typename: "conversations", - id: conversationId - }); + case "conversation-created": + updateConversationList({ ...fields, job_conversations, updated_at: updatedAt }); + break; - if (!cacheId) { - console.error(`Could not find conversation with id: ${conversationId}`); - return; - } + case "conversation-unarchived": + case "conversation-archived": + // Would like to someday figure out how to get this working without refetch queries, + // But I have but a solid 4 hours into it, and there are just too many weird occurrences + try { + const listQueryVariables = { offset: 0 }; + const detailsQueryVariables = { conversationId }; - if (type === "conversation-unarchived" || type === "conversation-archived") { - try { - const listQueryVariables = { offset: 0 }; - const detailsQueryVariables = { conversationId }; + // Refetch conversation list and details + await client.refetchQueries({ + include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], + variables: [ + { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, + { query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables } + ] + }); - // Refetch the conversation list and details queries - await client.refetchQueries({ - include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], - variables: [ - { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, - { query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables } - ] - }); + logLocal("handleConversationChanged - Refetched queries after state change", { conversationId, type }); + } catch (error) { + console.error("Error refetching queries after conversation state change:", error); + } + break; - console.log("Refetched conversation list and details after state change."); - } catch (error) { - console.error("Error refetching queries after conversation state change:", error); + case "tag-added": + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + job_conversations: (existing = []) => [...existing, ...job_conversations] + } + }); + break; + + case "tag-removed": + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + job_conversations: (existing = [], { readField }) => + existing.filter((jobRef) => readField("jobid", jobRef) !== fields.jobId) + } + }); + break; + + default: + logLocal("handleConversationChanged - Unhandled type", { type }); + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + ...Object.fromEntries( + Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)]) + ) + } + }); } - - return; + } catch (error) { + console.error("Error handling conversation changes:", { type, error }); } - // Handle other types of updates (e.g., marked read, tags added/removed) - client.cache.modify({ - id: cacheId, - fields: { - ...Object.fromEntries( - Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)]) - ), - ...(type === "conversation-marked-read" && { - messages_aggregate: () => ({ - __typename: "messages_aggregate", - aggregate: { __typename: "messages_aggregate_fields", count: 0 } - }) - }), - ...(type === "tag-added" && { - job_conversations: (existing = []) => [...existing, ...job_conversations] - }), - ...(type === "tag-removed" && { - job_conversations: (existing = [], { readField }) => - existing.filter((jobRef) => readField("jobid", jobRef) !== data.jobId) - }) - } - }); }; const handleNewMessage = ({ conversationId, message }) => { if (!conversationId || !message?.id || !message?.text) { + logLocal("handleNewMessage - Missing conversationId or message details", { conversationId, message }); return; } - logLocal("handleNewMessage", { conversationId, message }); + logLocal("handleNewMessage - Start", { conversationId, message }); - client.cache.modify({ - id: client.cache.identify({ __typename: "conversations", id: conversationId }), - fields: { - messages(existing = []) { - // Ensure that the `message` object matches the schema - const newMessageRef = client.cache.writeFragment({ - data: { - __typename: "messages", - id: message.id, - body: message.text, - selectedMedia: message.image_path || [], - imexshopid: message.userid, - status: message.status, - created_at: message.created_at, - read: message.read - }, - fragment: gql` - fragment NewMessage on messages { - id - body - selectedMedia - imexshopid - status - created_at - read - } - ` - }); + try { + // Add the new message to the cache + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + messages(existing = []) { + // Write the new message to the cache + const newMessageRef = client.cache.writeFragment({ + data: { + __typename: "messages", + id: message.id, + text: message.text, + selectedMedia: message.image_path || [], + imexshopid: message.userid, + status: message.status, + created_at: message.created_at, + read: message.read + }, + fragment: gql` + fragment NewMessage on messages { + id + text + selectedMedia + imexshopid + status + created_at + read + } + ` + }); - // Prevent duplicates by checking if the message already exists - const isDuplicate = existing.some( - (msgRef) => - client.cache.readFragment({ - id: msgRef.__ref, - fragment: gql` - fragment CheckMessage on messages { - id - } - ` - })?.id === message.id - ); - - // We already have it, so return the existing list - if (isDuplicate) { - return existing; + // The merge function defined in the cache will handle deduplication + return [...existing, newMessageRef]; } - - return [...existing, newMessageRef]; // Add the new message reference } - } - }); + }); + + logLocal("handleNewMessage - Message added to cache", { conversationId, messageId: message.id }); + } catch (error) { + console.error("handleNewMessage - Error modifying cache:", error); + } }; socket.on("new-message-summary", handleNewMessageSummary); diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 613d96288..3b7f8385b 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,4 +1,4 @@ -import { useQuery } from "@apollo/client"; +import { useApolloClient, useQuery } from "@apollo/client"; import axios from "axios"; import React, { useContext, useEffect, useState } from "react"; import { connect } from "react-redux"; @@ -17,19 +17,73 @@ const mapStateToProps = createStructuredSelector({ export default connect(mapStateToProps, null)(ChatConversationContainer); export function ChatConversationContainer({ bodyshop, selectedConversation }) { + const client = useApolloClient(); + const { socket } = useContext(SocketContext); + const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); + const { loading: convoLoading, error: convoError, data: convoData } = useQuery(GET_CONVERSATION_DETAILS, { variables: { conversationId: selectedConversation }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only" + fetchPolicy: "network-only" }); - const { socket } = useContext(SocketContext); + // Utility to update Apollo cache + const updateCacheWithReadMessages = (conversationId, messageIds) => { + if (!conversationId || !messageIds || messageIds.length === 0) return; + + messageIds.forEach((messageId) => { + client.cache.modify({ + id: client.cache.identify({ __typename: "messages", id: messageId }), + fields: { + read() { + return true; // Mark message as read + } + } + }); + }); + + // Optionally update aggregate unread count + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { + messages_aggregate(existingAggregate) { + const updatedAggregate = { + ...existingAggregate, + aggregate: { + ...existingAggregate.aggregate, + count: 0 // No unread messages remaining + } + }; + return updatedAggregate; + } + } + }); + }; + + // Handle WebSocket events + useEffect(() => { + if (!socket || !socket.connected) return; + + const handleConversationChange = (data) => { + if (data.type === "conversation-marked-read") { + const { conversationId, messageIds } = data; + console.log("Conversation change received:", data); + updateCacheWithReadMessages(conversationId, messageIds); + } + }; + + socket.on("conversation-changed", handleConversationChange); + + return () => { + socket.off("conversation-changed", handleConversationChange); + }; + }, [socket, client]); + + // Handle joining/leaving conversation useEffect(() => { - // Early gate, we have no socket, bail. if (!socket || !socket.connected) return; socket.emit("join-bodyshop-conversation", { @@ -45,25 +99,41 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { }; }, [selectedConversation, bodyshop, socket]); - const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); - - const unreadCount = - convoData && - convoData.conversations_by_pk && - convoData.conversations_by_pk.messages && - convoData.conversations_by_pk.messages.reduce((acc, val) => { - return !val.read && !val.isoutbound ? acc + 1 : acc; - }, 0); - + // Handle marking conversation as read const handleMarkConversationAsRead = async () => { - if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) { + if (!convoData || !selectedConversation || markingAsReadInProgress) return; + + const conversation = convoData.conversations_by_pk; + if (!conversation) { + console.warn(`No data found for conversation ID: ${selectedConversation}`); + return; + } + + const unreadMessageIds = conversation.messages + ?.filter((message) => !message.read && !message.isoutbound) + .map((message) => message.id); + + if (unreadMessageIds?.length > 0) { setMarkingAsReadInProgress(true); - await axios.post("/sms/markConversationRead", { - conversationid: selectedConversation, - imexshopid: bodyshop.imexshopid, - bodyshopid: bodyshop.id - }); - setMarkingAsReadInProgress(false); + + try { + const payload = { + conversation, + imexshopid: bodyshop?.imexshopid, + bodyshopid: bodyshop?.id + }; + + console.log("Marking conversation as read:", payload); + + await axios.post("/sms/markConversationRead", payload); + + // Update local cache + updateCacheWithReadMessages(selectedConversation, unreadMessageIds); + } catch (error) { + console.error("Error marking conversation as read:", error.response?.data || error.message); + } finally { + setMarkingAsReadInProgress(false); + } } }; diff --git a/client/src/utils/GraphQLClient.js b/client/src/utils/GraphQLClient.js index e0d409bd7..423c38e54 100644 --- a/client/src/utils/GraphQLClient.js +++ b/client/src/utils/GraphQLClient.js @@ -162,6 +162,22 @@ const cache = new InMemoryCache({ (incomingItem) => !existing.some((existingItem) => existingItem.__ref === incomingItem.__ref) ) ]; + return merged; + } + }, + messages: { + keyArgs: false, // Ignore arguments when determining uniqueness (like `order_by`). + merge(existing = [], incoming = [], { readField }) { + const existingIds = new Set(existing.map((message) => readField("id", message))); + + // Merge incoming messages, avoiding duplicates + const merged = [...existing]; + incoming.forEach((message) => { + if (!existingIds.has(readField("id", message))) { + merged.push(message); + } + }); + return merged; } } diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 72224dfb6..e70144775 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2569,6 +2569,9 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) { exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: uuid!) { update_messages(where: { conversationid: { _eq: $conversationId } }, _set: { read: true }) { + returning { + id + } affected_rows } } diff --git a/server/sms/status.js b/server/sms/status.js index 51d2f7ecb..0e29bbf8f 100644 --- a/server/sms/status.js +++ b/server/sms/status.js @@ -58,48 +58,49 @@ exports.status = async (req, res) => { logger.log("sms-status-update-error", "ERROR", "api", null, { msid: SmsSid, fields: { status: SmsStatus }, - error + stack: error.stack, + message: error.message }); res.status(500).json({ error: "Failed to update message status." }); } }; exports.markConversationRead = async (req, res) => { - const { conversationid, imexshopid, bodyshopid } = req.body; const { ioRedis, ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } } = req; + const { conversation, imexshopid, bodyshopid } = req.body; + + // Alternatively, support both payload formats + const conversationId = conversation?.id || req.body.conversationId; + + if (!conversationId || !imexshopid || !bodyshopid) { + return res.status(400).json({ error: "Invalid conversation data provided." }); + } try { - // Mark messages in the conversation as read const response = await client.request(queries.MARK_MESSAGES_AS_READ, { - conversationId: conversationid + conversationId }); - const updatedMessages = response.update_messages.affected_rows; - - logger.log("conversation-mark-read", "DEBUG", "api", null, { - conversationid, - imexshopid, - bodyshopid, - updatedMessages - }); + const updatedMessageIds = response.update_messages.returning.map((message) => message.id); const broadcastRoom = getBodyshopRoom(bodyshopid); ioRedis.to(broadcastRoom).emit("conversation-changed", { type: "conversation-marked-read", - conversationId: conversationid + conversationId, + affectedMessages: response.update_messages.affected_rows, + messageIds: updatedMessageIds }); - res.status(200).json({ success: true, message: "Conversation marked as read." }); - } catch (error) { - logger.log("conversation-mark-read-error", "ERROR", "api", null, { - conversationid, - imexshopid, - error + res.status(200).json({ + success: true, + message: "Conversation marked as read." }); + } catch (error) { + console.error("Error marking conversation as read:", error); res.status(500).json({ error: "Failed to mark conversation as read." }); } }; From 3ab471e6297626aaf658cd63bd32b8bba5f9a3d9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 22 Nov 2024 08:23:24 -0800 Subject: [PATCH 19/30] feature/IO-3000-messaging-sockets-migrations2 - - Final fix of unread messagages Signed-off-by: Dave Richer --- .../chat-popup/chat-popup.component.jsx | 37 ++++++++++++++----- client/src/redux/messaging/messaging.sagas.js | 2 - 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index e67929d11..f99738cb1 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -1,11 +1,11 @@ import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; -import { useLazyQuery, useQuery } from "@apollo/client"; +import { useApolloClient, useLazyQuery } from "@apollo/client"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; -import React, { useCallback, useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries"; +import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component"; @@ -28,13 +28,9 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh const { t } = useTranslation(); const [pollInterval, setPollInterval] = useState(0); const { socket } = useContext(SocketContext); + const client = useApolloClient(); // Apollo Client instance for cache operations - const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - ...(pollInterval > 0 ? { pollInterval } : {}) - }); - + // Lazy query for conversations const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -42,6 +38,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh ...(pollInterval > 0 ? { pollInterval } : {}) }); + // Socket connection status useEffect(() => { const handleSocketStatus = () => { if (socket?.connected) { @@ -66,6 +63,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh }; }, [socket]); + // Fetch conversations when chat becomes visible useEffect(() => { if (chatVisible) getConversations({ @@ -77,7 +75,26 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh }); }, [chatVisible, getConversations]); - const unreadCount = unreadData?.messages_aggregate?.aggregate?.count || 0; + // Get unread count from the cache + const unreadCount = (() => { + try { + const cachedData = client.readQuery({ + query: CONVERSATION_LIST_QUERY, + variables: { offset: 0 } + }); + + if (!cachedData?.conversations) return 0; + + // Aggregate unread message count + return cachedData.conversations.reduce((total, conversation) => { + const unread = conversation.messages_aggregate?.aggregate?.count || 0; + return total + unread; + }, 0); + } catch (error) { + console.warn("Unread count not found in cache:", error); + return 0; // Fallback if not in cache + } + })(); return ( diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index d2b428e1f..012c05618 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -4,9 +4,7 @@ import { all, call, put, select, takeLatest } from "redux-saga/effects"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { CONVERSATION_ID_BY_PHONE, - CONVERSATION_LIST_QUERY, CREATE_CONVERSATION, - GET_CONVERSATION_DETAILS, TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; From 49044e566998cf28b1b277925e6176a2117e6db6 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 22 Nov 2024 10:03:41 -0800 Subject: [PATCH 20/30] feature/IO-3000-messaging-sockets-migrations2 - - Missed a check Signed-off-by: Dave Richer --- client/src/pages/manage/manage.page.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index c1a8e561c..b39ee69c4 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -647,7 +647,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) { return ( <> - {true && } + From cbc8665636e04243d821b3135fb6e58ef0cb5d63 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 08:36:33 -0800 Subject: [PATCH 21/30] feature/IO-3000-messaging-sockets-migration2 - Handle some of the PR notes Signed-off-by: Dave Richer --- .../chat-archive-button.component.jsx | 13 +++- .../chat-conversation-list.component.jsx | 2 +- ...chat-conversation-title-tags.component.jsx | 13 +++- .../chat-conversation-title.component.jsx | 24 ++++--- .../chat-conversation.component.jsx | 13 +++- .../chat-conversation.container.jsx | 63 ++++++++++--------- .../chat-label/chat-label.component.jsx | 13 +++- .../chat-tag-ro/chat-tag-ro.container.jsx | 13 +++- 8 files changed, 109 insertions(+), 45 deletions(-) diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx index 2b8bcd054..6572cbefc 100644 --- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx +++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx @@ -4,8 +4,17 @@ import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatArchiveButton({ conversation, bodyshop }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +export function ChatArchiveButton({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); @@ -36,3 +45,5 @@ export default function ChatArchiveButton({ conversation, bodyshop }) { ); } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatArchiveButton); diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index a8f95f59d..fe71ee46e 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -64,7 +64,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); }; - // TODO: Can go back into virtuoso for additional fetch + // CAN DO: Can go back into virtuoso for additional fetch // endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom return ( diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index 944ade8b0..821ad1603 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -6,8 +6,17 @@ import { logImEXEvent } from "../../firebase/firebase.utils"; import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatConversationTitleTags({ jobConversations, bodyshop }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +export function ChatConversationTitleTags({ jobConversations, bodyshop }) { const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); const { socket } = useContext(SocketContext); @@ -66,3 +75,5 @@ export default function ChatConversationTitleTags({ jobConversations, bodyshop }
); } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitleTags); diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 7754ea347..7e5b045f5 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -6,19 +6,27 @@ import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conv import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatConversationTitle({ conversation, bodyshop }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +export function ChatConversationTitle({ conversation }) { return ( {conversation && conversation.phone_num} - + - - - + + + ); } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitle); diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index 4188fbabe..3334b5cbd 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -5,8 +5,17 @@ import ChatMessageListComponent from "../chat-messages-list/chat-message-list.co import ChatSendMessage from "../chat-send-message/chat-send-message.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import "./chat-conversation.styles.scss"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatConversationComponent({ +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +export function ChatConversationComponent({ subState, conversation, messages, @@ -31,3 +40,5 @@ export default function ChatConversationComponent({
); } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationComponent); diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 3b7f8385b..e2f7c6ae9 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,6 +1,6 @@ import { useApolloClient, useQuery } from "@apollo/client"; import axios from "axios"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import SocketContext from "../../contexts/SocketIO/socketContext"; @@ -14,8 +14,6 @@ const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -export default connect(mapStateToProps, null)(ChatConversationContainer); - export function ChatConversationContainer({ bodyshop, selectedConversation }) { const client = useApolloClient(); const { socket } = useContext(SocketContext); @@ -30,38 +28,40 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { fetchPolicy: "network-only" }); - // Utility to update Apollo cache - const updateCacheWithReadMessages = (conversationId, messageIds) => { - if (!conversationId || !messageIds || messageIds.length === 0) return; + const updateCacheWithReadMessages = useCallback( + (conversationId, messageIds) => { + if (!conversationId || !messageIds || messageIds.length === 0) return; - messageIds.forEach((messageId) => { + // Mark individual messages as read + messageIds.forEach((messageId) => { + client.cache.modify({ + id: client.cache.identify({ __typename: "messages", id: messageId }), + fields: { + read() { + return true; // Mark message as read + } + } + }); + }); + + // Update aggregate unread count for the conversation client.cache.modify({ - id: client.cache.identify({ __typename: "messages", id: messageId }), + id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { - read() { - return true; // Mark message as read + messages_aggregate(existingAggregate) { + return { + ...existingAggregate, + aggregate: { + ...existingAggregate.aggregate, + count: 0 // No unread messages remaining + } + }; } } }); - }); - - // Optionally update aggregate unread count - client.cache.modify({ - id: client.cache.identify({ __typename: "conversations", id: conversationId }), - fields: { - messages_aggregate(existingAggregate) { - const updatedAggregate = { - ...existingAggregate, - aggregate: { - ...existingAggregate.aggregate, - count: 0 // No unread messages remaining - } - }; - return updatedAggregate; - } - } - }); - }; + }, + [client.cache] + ); // Handle WebSocket events useEffect(() => { @@ -80,7 +80,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { return () => { socket.off("conversation-changed", handleConversationChange); }; - }, [socket, client]); + }, [socket, client, updateCacheWithReadMessages]); // Handle joining/leaving conversation useEffect(() => { @@ -143,7 +143,8 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { conversation={convoData ? convoData.conversations_by_pk : {}} messages={convoData ? convoData.conversations_by_pk.messages : []} handleMarkConversationAsRead={handleMarkConversationAsRead} - bodyshop={bodyshop} /> ); } + +export default connect(mapStateToProps, null)(ChatConversationContainer); diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx index e577bbf23..0764869ee 100644 --- a/client/src/components/chat-label/chat-label.component.jsx +++ b/client/src/components/chat-label/chat-label.component.jsx @@ -5,8 +5,17 @@ import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatLabel({ conversation, bodyshop }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export function ChatLabel({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(false); const [value, setValue] = useState(conversation.label); @@ -67,3 +76,5 @@ export default function ChatLabel({ conversation, bodyshop }) { ); } } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatLabel); diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index 6e01ea5ed..8d3476c7d 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -9,8 +9,17 @@ import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import ChatTagRo from "./chat-tag-ro.component"; import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import { connect } from "react-redux"; -export default function ChatTagRoContainer({ conversation, bodyshop }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = () => ({}); + +export function ChatTagRoContainer({ conversation, bodyshop }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const { socket } = useContext(SocketContext); @@ -86,3 +95,5 @@ export default function ChatTagRoContainer({ conversation, bodyshop }) { ); } + +export default connect(mapStateToProps, mapDispatchToProps)(ChatTagRoContainer); From 5e2c0f9c4a00639566f99b4299932d2f67d058bf Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 08:38:07 -0800 Subject: [PATCH 22/30] feature/IO-3000-messaging-sockets-migration2 - Handle some of the PR notes Signed-off-by: Dave Richer --- client/src/components/chat-popup/chat-popup.component.jsx | 2 +- client/src/redux/messaging/messaging.sagas.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index f99738cb1..7c0711fbc 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -31,7 +31,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh const client = useApolloClient(); // Apollo Client instance for cache operations // Lazy query for conversations - const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, { + const [getConversations, { loading, data, refetch }] = useLazyQuery(CONVERSATION_LIST_QUERY, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", skip: !chatVisible, diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index 012c05618..7194ea4dc 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -42,7 +42,7 @@ export function* openChatByPhone({ payload }) { } = yield client.query({ query: CONVERSATION_ID_BY_PHONE, variables: { phone: p.number }, - fetchPolicy: "no-cache" + fetchPolicy: "network-only" }); if (conversations.length === 0) { From 457a3b2d7adec8a00a3a95ae491913bd71c52314 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 08:42:02 -0800 Subject: [PATCH 23/30] feature/IO-3000-messaging-sockets-migration2 - Handle some of the PR notes Signed-off-by: Dave Richer --- .../chat-conversation-title.component.jsx | 5 +---- .../chat-conversation/chat-conversation.container.jsx | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 7e5b045f5..07b48716f 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -7,12 +7,9 @@ import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors.js"; import { connect } from "react-redux"; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop -}); +const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = () => ({}); diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index e2f7c6ae9..b477b436e 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -25,7 +25,8 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { data: convoData } = useQuery(GET_CONVERSATION_DETAILS, { variables: { conversationId: selectedConversation }, - fetchPolicy: "network-only" + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" }); const updateCacheWithReadMessages = useCallback( From 239c1502f90a5d077e736df348f0078dbb983931 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 09:57:18 -0800 Subject: [PATCH 24/30] feature/IO-3000-messaging-sockets-migration2 - Fix console warn in archive/unarchive if one query is not existent Signed-off-by: Dave Richer --- .../chat-affix/registerMessagingSocketHandlers.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 5b4f34ea1..d0ab4baa5 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -337,12 +337,18 @@ export const registerMessagingHandlers = ({ socket, client }) => { const listQueryVariables = { offset: 0 }; const detailsQueryVariables = { conversationId }; - // Refetch conversation list and details + // Check if conversation details exist in the cache + const detailsExist = !!client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: detailsQueryVariables + }); + + // Refetch conversation list await client.refetchQueries({ - include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS], + include: [CONVERSATION_LIST_QUERY, ...(detailsExist ? [GET_CONVERSATION_DETAILS] : [])], variables: [ { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, - { query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables } + ...(detailsExist ? [{ query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables }] : []) ] }); From 62dd3d7e8ec823f5285d7bd1fff329784cf70005 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 11:38:59 -0800 Subject: [PATCH 25/30] feature/IO-3000-messaging-sockets-migration2 - Final fixes around sync / archive / receive Signed-off-by: Dave Richer --- client/src/redux/messaging/messaging.sagas.js | 20 +++-- server/graphql-client/queries.js | 31 ++++++++ server/sms/receive.js | 73 ++++++++++--------- 3 files changed, 77 insertions(+), 47 deletions(-) diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index 7194ea4dc..fb519bcca 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -42,10 +42,13 @@ export function* openChatByPhone({ payload }) { } = yield client.query({ query: CONVERSATION_ID_BY_PHONE, variables: { phone: p.number }, - fetchPolicy: "network-only" + // THIS NEEDS TO REMAIN NO CACHE, IT CHECKS FOR NEW MESSAGES FOR SYNC + fetchPolicy: "no-cache" }); - if (conversations.length === 0) { + const existingConversation = conversations?.find((c) => c.phone_num === phone_num); + + if (!existingConversation) { // No conversation exists, create a new one const { data: { @@ -75,18 +78,17 @@ export function* openChatByPhone({ payload }) { // Set the newly created conversation as selected yield put(setSelectedConversation(createdConversation.id)); - } else if (conversations.length === 1) { - const conversation = conversations[0]; - let updatedConversation = conversation; + } else { + let updatedConversation = existingConversation; - if (conversation.archived) { + if (existingConversation.archived) { // Conversation is archived, unarchive it in the DB const { data: { update_conversations_by_pk: unarchivedConversation } } = yield client.mutate({ mutation: TOGGLE_CONVERSATION_ARCHIVE, variables: { - id: conversation.id, + id: existingConversation.id, archived: false } }); @@ -115,10 +117,6 @@ export function* openChatByPhone({ payload }) { } }); } - } else { - // Multiple conversations found - console.error("ERROR: Multiple conversations found."); - yield put(setSelectedConversation(null)); } } catch (error) { console.error("Error in openChatByPhone saga.", error); diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 4d82f0025..cf5e2b016 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2577,3 +2577,34 @@ exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: } } `; + +exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conversations_insert_input!]!) { + insert_conversations(objects: $conversation) { + returning { + id + phone_num + archived + label + unreadcnt + job_conversations { + jobid + conversationid + job { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + } + } + messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) { + aggregate { + count + } + } + created_at + updated_at + } + } +} +`; diff --git a/server/sms/receive.js b/server/sms/receive.js index 63c11a260..fef921912 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -34,6 +34,7 @@ exports.receive = async (req, res) => { } try { + // Step 1: Find the bodyshop and existing conversation const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, { mssid: req.body.MessagingServiceSid, phone: phone(req.body.From).phoneNumber @@ -44,53 +45,51 @@ exports.receive = async (req, res) => { } const bodyshop = response.bodyshops[0]; - const isNewConversation = bodyshop.conversations.length === 0; - const isDuplicate = bodyshop.conversations.length > 1; + const existingConversation = bodyshop.conversations[0]; // Expect only one conversation per phone number per bodyshop + let conversationid; let newMessage = { msid: req.body.SmsMessageSid, text: req.body.Body, image: !!req.body.MediaUrl0, - image_path: generateMediaArray(req.body) + image_path: generateMediaArray(req.body), + isoutbound: false, + userid: null // Add additional fields as necessary }; - if (isDuplicate) { - logger.log("sms-inbound-error", "ERROR", "api", null, { - ...loggerData, - messagingServiceSid: req.body.MessagingServiceSid, - type: "duplicate-phone" - }); - return res.status(400).json({ success: false, error: "Duplicate phone number" }); - } + if (existingConversation) { + // Use the existing conversation + conversationid = existingConversation.id; - if (isNewConversation) { - newMessage.conversation = { - data: { + // Unarchive the conversation if necessary + if (existingConversation.archived) { + await client.request(queries.UNARCHIVE_CONVERSATION, { + id: conversationid, + archived: false + }); + } + } else { + // Create a new conversation + const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, { + conversation: { bodyshopid: bodyshop.id, phone_num: phone(req.body.From).phoneNumber, archived: false } - }; - } else { - const existingConversation = bodyshop.conversations[0]; - - // Update the conversation to unarchive it - if (existingConversation.archived) { - await client.request(queries.UNARCHIVE_CONVERSATION, { - id: existingConversation.id, - archived: false - }); - } - - newMessage.conversationid = existingConversation.id; + }); + const createdConversation = newConversationResponse.insert_conversations.returning[0]; + conversationid = createdConversation.id; } - const query = isNewConversation ? queries.RECEIVE_MESSAGE : queries.INSERT_MESSAGE; - const variables = isNewConversation - ? { msg: newMessage } - : { msg: newMessage, conversationid: newMessage.conversationid }; + // Ensure `conversationid` is added to the message + newMessage.conversationid = conversationid; + + // Step 3: Insert the message into the conversation + const insertresp = await client.request(queries.INSERT_MESSAGE, { + msg: newMessage, + conversationid: conversationid + }); - const insertresp = await client.request(query, variables); const message = insertresp?.insert_messages?.returning?.[0]; const conversation = message?.conversation || null; @@ -98,6 +97,7 @@ exports.receive = async (req, res) => { throw new Error("Conversation data is missing from the response."); } + // Step 4: Notify clients through Redis const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); const conversationRoom = getBodyshopConversationRoom({ bodyshopId: conversation.bodyshop.id, @@ -113,19 +113,20 @@ exports.receive = async (req, res) => { ioRedis.to(broadcastRoom).emit("new-message-summary", { ...commonPayload, - existingConversation: !isNewConversation, - newConversation: isNewConversation ? conversation : null, + existingConversation: !!existingConversation, + newConversation: !existingConversation ? conversation : null, summary: true }); ioRedis.to(conversationRoom).emit("new-message-detailed", { newMessage: message, ...commonPayload, - newConversation: isNewConversation ? conversation : null, - existingConversation: !isNewConversation, + newConversation: !existingConversation ? conversation : null, + existingConversation: !!existingConversation, summary: false }); + // Step 5: Send FCM notification const fcmresp = await admin.messaging().send({ topic: `${message.conversation.bodyshop.imexshopid}-messaging`, notification: { From c3c66f96465afaefd60e239b4e007d435217da22 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 12:46:10 -0800 Subject: [PATCH 26/30] feature/IO-3000-messaging-sockets-migration2 - Final Modifications Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 94 ++++++------------- .../chat-send-message.component.jsx | 8 -- client/src/redux/messaging/messaging.sagas.js | 91 +++++++++--------- server/web-sockets/redisSocketEvents.js | 13 --- 4 files changed, 77 insertions(+), 129 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index d0ab4baa5..09b405861 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -46,16 +46,20 @@ export const registerMessagingHandlers = ({ socket, client }) => { variables: queryVariables }); + const existingConversations = queryResults?.conversations || []; const enrichedConversation = enrichConversation(newConversation, isoutbound); - client.cache.modify({ - id: "ROOT_QUERY", - fields: { - conversations(existingConversations = []) { - return [enrichedConversation, ...existingConversations]; + // Avoid adding duplicate conversations + if (!existingConversations.some((conv) => conv.id === enrichedConversation.id)) { + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + conversations(existingConversations = []) { + return [enrichedConversation, ...existingConversations]; + } } - } - }); + }); + } } catch (error) { console.error("Error updating cache for new conversation:", error); } @@ -192,7 +196,10 @@ export const registerMessagingHandlers = ({ socket, client }) => { } }); - logLocal("handleNewMessageDetailed - Message appended successfully", { conversationId, newMessage }); + logLocal("handleNewMessageDetailed - Message appended successfully", { + conversationId, + newMessage + }); } catch (error) { console.error("Error updating conversation messages in cache:", error); } @@ -255,7 +262,10 @@ export const registerMessagingHandlers = ({ socket, client }) => { } }); - logLocal("handleMessageChanged - Message updated successfully", { messageId: message.id, type: message.type }); + logLocal("handleMessageChanged - Message updated successfully", { + messageId: message.id, + type: message.type + }); } catch (error) { console.error("handleMessageChanged - Error modifying cache:", error); } @@ -348,11 +358,21 @@ export const registerMessagingHandlers = ({ socket, client }) => { include: [CONVERSATION_LIST_QUERY, ...(detailsExist ? [GET_CONVERSATION_DETAILS] : [])], variables: [ { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, - ...(detailsExist ? [{ query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables }] : []) + ...(detailsExist + ? [ + { + query: GET_CONVERSATION_DETAILS, + variables: detailsQueryVariables + } + ] + : []) ] }); - logLocal("handleConversationChanged - Refetched queries after state change", { conversationId, type }); + logLocal("handleConversationChanged - Refetched queries after state change", { + conversationId, + type + }); } catch (error) { console.error("Error refetching queries after conversation state change:", error); } @@ -393,60 +413,8 @@ export const registerMessagingHandlers = ({ socket, client }) => { } }; - const handleNewMessage = ({ conversationId, message }) => { - if (!conversationId || !message?.id || !message?.text) { - logLocal("handleNewMessage - Missing conversationId or message details", { conversationId, message }); - return; - } - - logLocal("handleNewMessage - Start", { conversationId, message }); - - try { - // Add the new message to the cache - client.cache.modify({ - id: client.cache.identify({ __typename: "conversations", id: conversationId }), - fields: { - messages(existing = []) { - // Write the new message to the cache - const newMessageRef = client.cache.writeFragment({ - data: { - __typename: "messages", - id: message.id, - text: message.text, - selectedMedia: message.image_path || [], - imexshopid: message.userid, - status: message.status, - created_at: message.created_at, - read: message.read - }, - fragment: gql` - fragment NewMessage on messages { - id - text - selectedMedia - imexshopid - status - created_at - read - } - ` - }); - - // The merge function defined in the cache will handle deduplication - return [...existing, newMessageRef]; - } - } - }); - - logLocal("handleNewMessage - Message added to cache", { conversationId, messageId: message.id }); - } catch (error) { - console.error("handleNewMessage - Error modifying cache:", error); - } - }; - socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); - socket.on("new-message", handleNewMessage); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); }; diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index e86531ee4..7aa91c8e6 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -49,14 +49,6 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi imexshopid: bodyshop.imexshopid }; sendMessage(newMessage); - if (socket) { - const lastMessage = conversation.messages?.[conversation.messages.length - 1]; // Get the last message - socket.emit("message-added", { - conversationId: conversation.id, - bodyshopId: bodyshop.id, - message: lastMessage - }); - } setSelectedMedia( selectedMedia.map((i) => { return { ...i, isSelected: false }; diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index fb519bcca..e2cbb3bc8 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -33,22 +33,62 @@ export function* openChatByPhone({ payload }) { logImEXEvent("messaging_open_by_phone"); const { socket, phone_num, jobid } = payload; if (!socket || !phone_num) return; + const p = parsePhoneNumber(phone_num, "CA"); const bodyshop = yield select(selectBodyshop); try { + // Fetch conversations including archived ones const { data: { conversations } } = yield client.query({ query: CONVERSATION_ID_BY_PHONE, variables: { phone: p.number }, - // THIS NEEDS TO REMAIN NO CACHE, IT CHECKS FOR NEW MESSAGES FOR SYNC - fetchPolicy: "no-cache" + fetchPolicy: "no-cache" // Ensure the query always gets the latest data }); - const existingConversation = conversations?.find((c) => c.phone_num === phone_num); + const existingConversation = conversations?.find((c) => c.phone_num === p.number); - if (!existingConversation) { + if (existingConversation) { + let updatedConversation = existingConversation; + + if (existingConversation.archived) { + // If the conversation is archived, unarchive it + const { + data: { update_conversations_by_pk: unarchivedConversation } + } = yield client.mutate({ + mutation: TOGGLE_CONVERSATION_ARCHIVE, + variables: { + id: existingConversation.id, + archived: false + } + }); + + updatedConversation = unarchivedConversation; + + // Emit an event indicating the conversation was unarchived + socket.emit("conversation-modified", { + type: "conversation-unarchived", + conversationId: unarchivedConversation.id, + bodyshopId: bodyshop.id, + archived: false + }); + } + + // Set the unarchived or already active conversation as selected + yield put(setSelectedConversation(updatedConversation.id)); + + // Add job tag if needed + if (jobid && !updatedConversation.job_conversations.find((jc) => jc.jobid === jobid)) { + yield client.mutate({ + mutation: INSERT_CONVERSATION_TAG, + variables: { + conversationId: updatedConversation.id, + jobId: jobid + } + }); + } + } else { // No conversation exists, create a new one const { data: { @@ -67,9 +107,9 @@ export function* openChatByPhone({ payload }) { } }); - const createdConversation = newConversations[0]; // Get the newly created conversation + const createdConversation = newConversations[0]; - // Emit event for new conversation with full details + // Emit event for the new conversation with full details socket.emit("conversation-modified", { bodyshopId: bodyshop.id, type: "conversation-created", @@ -78,45 +118,6 @@ export function* openChatByPhone({ payload }) { // Set the newly created conversation as selected yield put(setSelectedConversation(createdConversation.id)); - } else { - let updatedConversation = existingConversation; - - if (existingConversation.archived) { - // Conversation is archived, unarchive it in the DB - const { - data: { update_conversations_by_pk: unarchivedConversation } - } = yield client.mutate({ - mutation: TOGGLE_CONVERSATION_ARCHIVE, - variables: { - id: existingConversation.id, - archived: false - } - }); - - updatedConversation = unarchivedConversation; - - // Emit the unarchived event only once - socket.emit("conversation-modified", { - type: "conversation-unarchived", - conversationId: unarchivedConversation.id, - bodyshopId: bodyshop.id, - archived: false - }); - } - - // Open the conversation - yield put(setSelectedConversation(updatedConversation.id)); - - // Check and add job tag if needed - if (jobid && !updatedConversation.job_conversations.find((jc) => jc.jobid === jobid)) { - yield client.mutate({ - mutation: INSERT_CONVERSATION_TAG, - variables: { - conversationId: updatedConversation.id, - jobId: jobid - } - }); - } } } catch (error) { console.error("Error in openChatByPhone saga.", error); diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 366461adc..9fd7e5a93 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -188,19 +188,6 @@ const redisSocketEvents = ({ } }; - const messageAdded = ({ bodyshopId, conversationId, message }) => { - try { - const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); - io.to(room).emit("new-message", { message, conversationId }); - } catch (error) { - logger.log("Failed to handle new message", "error", "io-redis", null, { - error: error.message, - stack: error.stack - }); - } - }; - - socket.on("message-added", messageAdded); socket.on("conversation-modified", conversationModified); socket.on("join-bodyshop-conversation", joinConversationRoom); socket.on("leave-bodyshop-conversation", leaveConversationRoom); From 5c30f33dac9e1877b3ee994df678d269555178b5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 25 Nov 2024 13:45:09 -0800 Subject: [PATCH 27/30] feature/IO-3000-messaging-sockets-migration2 - Additional logging Signed-off-by: Dave Richer --- server/web-sockets/redisSocketEvents.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 9fd7e5a93..d96fcb7d7 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -182,6 +182,9 @@ const redisSocketEvents = ({ }); } catch (error) { logger.log("Failed to handle conversation modification", "error", "io-redis", null, { + bodyshopId, + conversationId, + fields, error: error.message, stack: error.stack }); From d8311c516339985d398a17ca2e21d94b6b266cb2 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 26 Nov 2024 07:00:54 -0800 Subject: [PATCH 28/30] feature/IO-3000-messaging-sockets-migration2 - remove unused dep Signed-off-by: Dave Richer --- .../chat-send-message/chat-send-message.component.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index 7aa91c8e6..d95bf039b 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -1,6 +1,6 @@ import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; import { Input, Spin } from "antd"; -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -10,7 +10,6 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging. import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -26,7 +25,6 @@ const mapDispatchToProps = (dispatch) => ({ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) { const inputArea = useRef(null); const [selectedMedia, setSelectedMedia] = useState([]); - const { socket } = useContext(SocketContext); useEffect(() => { inputArea.current.focus(); From e9dfba7d3159d81a9ef350bc5a25e4c411188c88 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 26 Nov 2024 08:39:21 -0800 Subject: [PATCH 29/30] feature/IO-3000-messaging-sockets-migration2 - Extra checks on scroll to index to prevent console warns when no messages exist in the conversation. Signed-off-by: Dave Richer --- .../chat-message-list.component.jsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx index 81d3296fc..00b9204cc 100644 --- a/client/src/components/chat-messages-list/chat-message-list.component.jsx +++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx @@ -3,33 +3,41 @@ import { Virtuoso } from "react-virtuoso"; import { renderMessage } from "./renderMessage"; import "./chat-message-list.styles.scss"; +const SCROLL_DELAY_MS = 50; +const INITIAL_SCROLL_DELAY_MS = 100; + export default function ChatMessageListComponent({ messages }) { const virtuosoRef = useRef(null); // Scroll to the bottom after a short delay when the component mounts useEffect(() => { const timer = setTimeout(() => { - if (virtuosoRef.current) { + if (virtuosoRef?.current?.scrollToIndex && messages?.length) { virtuosoRef.current.scrollToIndex({ index: messages.length - 1, behavior: "auto" // Instantly scroll to the bottom }); } - }, 100); // Delay of 100ms to allow rendering + }, INITIAL_SCROLL_DELAY_MS); + + // Cleanup the timeout on unmount return () => clearTimeout(timer); - }, [messages.length]); // Run only once on component mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // ESLint is disabled for this line because we only want this to load once (valid exception) // Scroll to the bottom after the new messages are rendered useEffect(() => { - if (virtuosoRef.current) { - // Allow the DOM and Virtuoso to fully render the new data - setTimeout(() => { + if (virtuosoRef?.current?.scrollToIndex && messages?.length) { + const timeout = setTimeout(() => { virtuosoRef.current.scrollToIndex({ index: messages.length - 1, align: "end", // Ensure the last message is fully visible behavior: "smooth" // Smooth scrolling }); - }, 50); // Slight delay to ensure layout recalculates + }, SCROLL_DELAY_MS); // Slight delay to ensure layout recalculates + + // Cleanup timeout on dependency changes + return () => clearTimeout(timeout); } }, [messages]); // Triggered when new messages are added From 63397769d20cbba2a7aede51280f22eea1d9fee9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 26 Nov 2024 10:29:57 -0800 Subject: [PATCH 30/30] feature/IO-3000-messaging-sockets-migration2 - Take last conversation if more than one exists. Signed-off-by: Dave Richer --- client/src/graphql/conversations.queries.js | 1 + client/src/redux/messaging/messaging.sagas.js | 7 ++++++- server/graphql-client/queries.js | 1 + server/sms/receive.js | 7 ++++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/client/src/graphql/conversations.queries.js b/client/src/graphql/conversations.queries.js index 33009ffbe..2379eb922 100644 --- a/client/src/graphql/conversations.queries.js +++ b/client/src/graphql/conversations.queries.js @@ -96,6 +96,7 @@ export const CONVERSATION_ID_BY_PHONE = gql` archived label unreadcnt + created_at job_conversations { jobid conversationid diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index e2cbb3bc8..644a940c5 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -47,7 +47,12 @@ export function* openChatByPhone({ payload }) { fetchPolicy: "no-cache" // Ensure the query always gets the latest data }); - const existingConversation = conversations?.find((c) => c.phone_num === p.number); + // Sort conversations by `updated_at` or `created_at` and pick the last one for the given phone number + const sortedConversations = conversations + ?.filter((c) => c.phone_num === p.number) // Filter to match the phone number + .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); // Sort by `updated_at` + + const existingConversation = sortedConversations?.[sortedConversations.length - 1] || null; if (existingConversation) { let updatedConversation = existingConversation; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 46837b6b6..da2aaac89 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -4,6 +4,7 @@ query FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID($mssid: String!, $phone: String!) { id conversations(where: { phone_num: { _eq: $phone } }) { id + created_at } } }`; diff --git a/server/sms/receive.js b/server/sms/receive.js index fef921912..99ff90af6 100644 --- a/server/sms/receive.js +++ b/server/sms/receive.js @@ -45,7 +45,12 @@ exports.receive = async (req, res) => { } const bodyshop = response.bodyshops[0]; - const existingConversation = bodyshop.conversations[0]; // Expect only one conversation per phone number per bodyshop + + // Sort conversations by `updated_at` (or `created_at`) and pick the last one + const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + const existingConversation = sortedConversations.length + ? sortedConversations[sortedConversations.length - 1] + : null; let conversationid; let newMessage = {