├── .gitignore ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── inventorydemo ├── README.md ├── build │ ├── bundle.js │ ├── bundle.js.LICENSE.txt │ ├── images │ │ ├── 50.jpeg │ │ ├── Free.jpeg │ │ ├── Portrait.jpeg │ │ ├── beautiful life.jpeg │ │ ├── body & arms.jpeg │ │ ├── hold me in your arms.jpeg │ │ ├── keep it turned on.jpeg │ │ ├── logo.png │ │ ├── my red book.jpeg │ │ ├── transparent.png │ │ └── whenever you need.jpeg │ └── index.html ├── client │ ├── App.jsx │ ├── components │ │ ├── Display.jsx │ │ ├── InventoryList.jsx │ │ └── Item.jsx │ ├── index.css │ ├── index.js │ └── services │ │ ├── events.js │ │ └── inventory.js └── public │ ├── images │ ├── 50.jpeg │ ├── Free.jpeg │ ├── Portrait.jpeg │ ├── beautiful life.jpeg │ ├── body & arms.jpeg │ ├── hold me in your arms.jpeg │ ├── keep it turned on.jpeg │ ├── logo.png │ ├── my red book.jpeg │ ├── transparent.png │ └── whenever you need.jpeg │ └── index.html ├── libraries ├── clientlib │ ├── README.md │ ├── customHook.js │ └── package.json └── serverlib │ ├── README.md │ ├── database │ └── databaseConnect.js │ ├── events │ └── eventHelperFuncs.js │ ├── package.json │ └── setupWebsocket.js ├── package-lock.json ├── package.json ├── server ├── controllers │ └── inventoryController.js ├── keys │ ├── server.crt │ └── server.key ├── models │ └── inventoryModel.js ├── routes │ └── inventory.js ├── server.js └── utils │ └── utils.js ├── storedemo ├── README.md ├── build │ ├── bundle.js │ ├── bundle.js.LICENSE.txt │ ├── images │ │ ├── 50.jpeg │ │ ├── Free.jpeg │ │ ├── Portrait.jpeg │ │ ├── beautiful life.jpeg │ │ ├── body & arms.jpeg │ │ ├── hold me in your arms.jpeg │ │ ├── keep it turned on.jpeg │ │ ├── logo.png │ │ ├── my red book.jpeg │ │ ├── transparent.png │ │ └── whenever you need.jpeg │ └── index.html ├── client │ ├── App.jsx │ ├── components │ │ ├── Cart.jsx │ │ ├── Display.jsx │ │ ├── InventoryList.jsx │ │ └── Item.jsx │ ├── index.css │ ├── index.js │ └── services │ │ ├── events.js │ │ ├── inventory.js │ │ └── ytapi.js └── public │ ├── images │ ├── 50.jpeg │ ├── Free.jpeg │ ├── Portrait.jpeg │ ├── beautiful life.jpeg │ ├── body & arms.jpeg │ ├── hold me in your arms.jpeg │ ├── keep it turned on.jpeg │ ├── logo.png │ ├── my red book.jpeg │ ├── transparent.png │ └── whenever you need.jpeg │ └── index.html └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 LiveStateDB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 4 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 11 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | [![Contributors][contributors-shield]][contributors-url] 15 | [![Forks][forks-shield]][forks-url] 16 | [![Stargazers][stars-shield]][stars-url] 17 | [![Issues][issues-shield]][issues-url] 18 | [![MIT License][license-shield]][license-url] 19 | [![LinkedIn][linkedin-shield]][linkedin-url] 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 |

LiveStateDB

29 | 30 |

31 | Automatically update state based on database changes 32 |
33 | 35 | Report Bug or Request Feature 36 |

37 |
38 | 39 | ## About 40 | 41 | 42 | 43 | LiveStateDB is a database subscription API that enables developers to make state reflect database changes in real time. Developers will have access to a custom hook, ```useSubscribe()```, to implement in your front-end codebase, and a back-end library which allows the server to interface with your database. Currently, LiveStateDB only supports MongoDB. 44 | 45 | 46 | ### Built With 47 | 48 | [![MongoDB][MongoDB]][MongoDB-url]
49 | [![Redis][Redis]][Redis-url]
50 | [![Socket.io][Socket.io]][Socket.io-url]
51 | 52 |
53 | 54 | # Getting Started 55 | 56 | LiveStateDB features a client side library and a server side library that can be installed via npm or yarn with the following commands. The libraries need to be installed on both sides in order to make use of LiveStateDB's real time updates. 57 | 58 | ```js 59 | npm install @livestatedb/client 60 | yarn install @livestatedb/client 61 | 62 | npm install @livestatedb/server 63 | yarn install @livestatedb/server 64 | ``` 65 | 66 | ## Using the hook useSubscribe( ) 67 | 68 |
69 | 70 | First import the module into the file you wish to use it. 71 | 72 | ```js 73 | import { useSubscribe } from '@livestatedb/client'; 74 | ``` 75 | 76 | When useSubscribe is called, it will open a web socket through which database updates get passed when changes occur that state is 'subscribed to'. 77 | 78 | useSubscribe takes one argument: an object with three key-value pairs that correspond with the database name, collection name, and query to a database. Here's an example. 79 | 80 | ```js 81 | const options = { 82 | database: 'DBname', 83 | collection: 'someCollectionName', 84 | query: {} 85 | }; 86 | ``` 87 | 88 | useSubscribe returns the way to reference state in your front-end code, and a function that ends that piece of state's subscription to the database. 89 | 90 | ```js 91 | const [ state, endSubscription ] = useSubscribe(options); 92 | ``` 93 | 94 | _**NOTE!**_
95 | useSubscribe will only receive updates from the database that match the options passed-in _at the time it was called_. If a database change occurs that would match the query parameters, it will not automatically be included in what useSubscribe is subscribed to. You would need to re-render the page to call useSubscribe again to capture this. 96 | 97 | Additionally, your query filter parameters should be constant. For example, if you are building a dashboard for managing inventory, your filter parameters could include condition: (new, like new, fair, poor), but should not include parameters such as quantity: (>50, <=50). In this example, if you wanted to be subscribed to data that had a certain quantity, you would need write your options object, and specifically your query parameters, to include all of the data you're interested in, regardless of quantity. You would then be able to filter out only the data you're interested in in your front-end codebase. 98 | 99 |
100 |
101 | 102 | ## Using the back-end library 103 | 104 |
105 | 106 | useSubscribe won't work until you pass information about your MongoDB and Redis into a function that opens a web socket and connects everything. Require the module in wherever you instantiate an http server, for example server.js. Feel free to give it a label or to to simply invoke the required-in function with the necessary arguments. 107 | 108 | ```js 109 | const liveStateDB = require('@livestatedb/server'); 110 | liveStateDB(httpServer, databaseInfo); 111 | ``` 112 | OR 113 | ```js 114 | require('@livestatedb/server')(httpServer, databaseInfo); 115 | ``` 116 | 117 | The second paramater, databaseInfo, must be an object with two key-value pairs - these are for passing in information about your MongoDB database and Redis, respectively. The values are both objects that contain connection information for each database. See example below. 118 | 119 | ```js 120 | const databaseInfo = 121 | { 122 | mongoDbOptions: 123 | { 124 | uri: "mongodb+srv://name:12345678910abcde@cluster0.aabbcc.mongodb.net/?retryWrites=true&w=majority" 125 | }, 126 | redisDbOptions: 127 | { 128 | host: 'redis-00000.c00.us-east-0-0.ec2.cloud.redislabs.com', 129 | port: 15711, 130 | password: 'lkajsdf092j3jlsdfmop3jfspdkgpoi', 131 | family: 4 132 | }, 133 | } 134 | ``` 135 | 136 | That's it! Your front-end will now be able to display or work with the subscribed database data as it updates in real time - regardless of where the change originates from. 137 | 138 | Thanks for your interest in LiveStateDB, and we hope this improves your development experience. 139 | 140 |

(back to top)

141 | 142 |
143 |
144 | 145 | # Contributing 146 | 147 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 148 | 149 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 150 | Don't forget to give the project a star! Thanks again! 151 | 152 | ## Starting Notes 153 | - Main branch is only for production 154 | - Dev branch is for development. Two person review process for pull requests to the dev and main branch. 155 | 156 | ## Starting off 157 | 1. Clone main repository to local machine 158 | 2. git checkout -b [name/feature] -> Create feature branch off of dev 159 | 3. Commit to your local feature branch often! 160 | 161 | ## Pushing changes to the dev repo 162 | 1. 'Git checkout dev' (locally switch to dev branch) 163 | 2. 'Git pull origin dev' (Pull updates of dev down to your local system) 164 | 3. 'Git checkout [your branch] (switch back to your branch locally) 165 | 4. 'Git merge dev' (Brings dev into your local branch) 166 | 5. Resolve conflicts or :q if there aren't any 167 | 6. 'Git push origin ' (Push merged branch up to github) 168 | 7. Create a pull request in github from => dev 169 | 8. Repeat as needed 170 | 9. When ready to publish main, do step 7 but from dev => main 171 | 172 | 173 | 181 | 182 | ## License 183 | 184 | Distributed under the MIT License. See `LICENSE.txt` for more information. 185 | 186 | # Contact 187 | 188 |

Kevin Richardson LinkedIn | GitHub

189 |

Stephanie Page LinkedIn | GitHub

190 |

David Cheng LinkedIn | GitHub

191 |

Evan Deam LinkedIn | GitHub

192 | 193 |

(back to top)

194 | 195 | 196 | 210 | 211 | 212 | 213 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/LiveStateDB?style=for-the-badge 214 | [contributors-url]: https://github.com/oslabs-beta/LiveStateDB/graphs/contributors 215 | 216 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/LiveStateDB?style=for-the-badge 217 | [forks-url]: https://github.com/oslabs-beta/LiveStateDB/network/members 218 | 219 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/LiveStateDB?style=for-the-badge 220 | [stars-url]: https://github.com/oslabs-beta/LiveStateDB/stargazers 221 | 222 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/LiveStateDB?style=for-the-badge 223 | [issues-url]: https://github.com/oslabs-beta/LiveStateDB/issues 224 | 225 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/LiveStateDB?style=for-the-badge 226 | [license-url]: https://github.com/oslabs-beta/LiveStateDB/blob/master/LICENSE.txt 227 | 228 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 229 | [linkedin-url]: https://www.linkedin.com/company/livestatedb/about/?viewAsMember=true 230 | 231 | [product-screenshot]: images/screeenshot2.png 232 | 233 | 234 | [Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white 235 | [Next-url]: https://nextjs.org/ 236 | [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D 237 | [Vue-url]: https://vuejs.org/ 238 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white 239 | [Angular-url]: https://angular.io/ 240 | [Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 241 | [Svelte-url]: https://svelte.dev/ 242 | [Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white 243 | [Laravel-url]: https://laravel.com 244 | [Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white 245 | [Bootstrap-url]: https://getbootstrap.com 246 | [JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white 247 | [JQuery-url]: https://jquery.com 248 | 249 | [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB 250 | [React-url]: https://reactjs.org/ 251 | [ReactRouter]: https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white 252 | [MongoDB]: https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white 253 | [MongoDB-url]: https://www.mongodb.com/ 254 | [ElephantSQL]: https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white 255 | [ElephantSQL-url]: https://www.elephantsql.com/ 256 | [Webpack]: https://img.shields.io/badge/webpack-%238DD6F9.svg?style=for-the-badge&logo=webpack&logoColor=black 257 | [Webpack-url]: https://webpack.js.org/plugins/html-webpack-plugin/ 258 | [Redis]: https://img.shields.io/badge/-Redis-DC382D?style=for-the-badge&logo=redis&logoColor=white 259 | [Redis-url]: https://redis.io/ 260 | [Socket.io]: https://img.shields.io/badge/-Socket.io-010101?style=for-the-badge&logo=Socket.io&logoColor=white 261 | [Socket.io-url]: https://socket.io/ 262 | -------------------------------------------------------------------------------- /inventorydemo/README.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | A template repository for a MongoDB - Express - React (w/ Redux) - Node project. 4 | 5 | -------------------------------------------------------------------------------- /inventorydemo/build/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | 3 | /** 4 | * @license React 5 | * react-dom.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * @license React 15 | * react.production.min.js 16 | * 17 | * Copyright (c) Facebook, Inc. and its affiliates. 18 | * 19 | * This source code is licensed under the MIT license found in the 20 | * LICENSE file in the root directory of this source tree. 21 | */ 22 | 23 | /** 24 | * @license React 25 | * scheduler.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | //! Can pass props to Display {database, collection, query}, then just pass those variables as args into useSubscribe(database, collection, query) 34 | -------------------------------------------------------------------------------- /inventorydemo/build/images/50.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/50.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/Free.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/Free.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/Portrait.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/Portrait.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/beautiful life.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/beautiful life.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/body & arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/body & arms.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/hold me in your arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/hold me in your arms.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/keep it turned on.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/keep it turned on.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/logo.png -------------------------------------------------------------------------------- /inventorydemo/build/images/my red book.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/my red book.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/transparent.png -------------------------------------------------------------------------------- /inventorydemo/build/images/whenever you need.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/build/images/whenever you need.jpeg -------------------------------------------------------------------------------- /inventorydemo/build/index.html: -------------------------------------------------------------------------------- 1 | inventory Demo
-------------------------------------------------------------------------------- /inventorydemo/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Display from './components/Display.jsx' 3 | 4 | const App = () => { 5 | 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /inventorydemo/client/components/Display.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from "react" 2 | import InventoryList from './InventoryList.jsx' 3 | import { changeSingleInventoryField } from '../services/inventory' 4 | import { useSubscribe } from "../../../libraries/clientlib/customHook"; 5 | 6 | //! Can pass props to Display {database, collection, query}, then just pass those variables as args into useSubscribe(database, collection, query) 7 | const Display = () => { 8 | 9 | const [ inventoryHookOptions, setInventoryHookOptions] = useState({ 10 | database: 'inventoryDemo', 11 | collection: 'inventoryitems', 12 | query: {}, 13 | }) 14 | 15 | console.log('we are in Display React Component') 16 | const [ inventoryList, endSubscription ] = useSubscribe({ 17 | database: 'inventoryDemo', 18 | collection: 'inventoryitems', 19 | query: {}, 20 | }); 21 | 22 | //increment/decrement click function 23 | const handleIncDecClick = (id, field, value) => { 24 | changeSingleInventoryField(id, field, value) 25 | } 26 | 27 | return ( 28 |
29 |
30 |

31 | 32 | Rick Rollin' Records 33 | 34 |

35 |
36 |
37 |

Inventory Manager

38 | 42 |
43 |
44 | ); 45 | } 46 | 47 | export default Display; -------------------------------------------------------------------------------- /inventorydemo/client/components/InventoryList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | import Item from './Item.jsx' 3 | 4 | 5 | const InventoryList = ({ inventoryList, handleIncDecClick }) => { 6 | 7 | 8 | const inventory = []; 9 | let i = 0; 10 | 11 | inventory.push( 12 | 21 | ) 22 | 23 | //for every item in the database we add an item component for rendering 24 | for(let items in inventoryList){ 25 | inventory.push( 26 | 36 | ) 37 | i++; 38 | } 39 | 40 | return ( 41 |
42 | { inventory } 43 |
44 | ); 45 | } 46 | 47 | export default InventoryList; -------------------------------------------------------------------------------- /inventorydemo/client/components/Item.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | const Item = ( 4 | { item, 5 | description, 6 | quantity, 7 | price, 8 | handleIncDecClick, 9 | id, 10 | className 11 | }) => { 12 | 13 | return( 14 |
15 | 16 | {item} 17 | 18 | 19 | {description} 20 | 21 | 22 | {quantity} 23 |
24 | {handleIncDecClick && 25 | 28 | } 29 | {handleIncDecClick && 30 | 33 | } 34 |
35 |
36 | 37 | {price} 38 | 39 |
40 | ) 41 | } 42 | 43 | export default Item; -------------------------------------------------------------------------------- /inventorydemo/client/index.css: -------------------------------------------------------------------------------- 1 | /* ul > li { 2 | display: inline-block; 3 | } 4 | 5 | li { 6 | padding-left: 20px; 7 | } */ 8 | 9 | * { 10 | background-color: #f1f5f8; 11 | color: #0D0D0D; 12 | padding: 0px; 13 | margin: 0px; 14 | font-family: sans-serif; 15 | } 16 | 17 | h1 { 18 | font-size: 8vw; 19 | text-align: center; 20 | } 21 | 22 | h2 { 23 | font-size: 5vw; 24 | text-align: center; 25 | margin-bottom: 5px; 26 | } 27 | 28 | .rickPicRight { 29 | width: 5%; 30 | margin-left: 5px; 31 | } 32 | 33 | .rickPicLeft { 34 | width: 5%; 35 | margin-right: 5px; 36 | transform: scaleX(-1); 37 | } 38 | 39 | .header { 40 | margin-top: 10px; 41 | display: flex; 42 | } 43 | 44 | .display { 45 | border: solid 2px #383838; 46 | margin: auto; 47 | width: 90%; 48 | height: 90%; 49 | padding: 5px; 50 | } 51 | 52 | .row { 53 | display: flex; 54 | justify-content: space-evenly; 55 | margin-bottom: -1px; 56 | } 57 | 58 | .values{ 59 | color: #232323; 60 | border: solid 1px #808080; 61 | border-radius: 0px; 62 | width: 100%; 63 | text-align: center; 64 | margin-left: -1px; 65 | } 66 | 67 | .label { 68 | display: flex; 69 | justify-content: space-evenly; 70 | margin-bottom: 5px; 71 | font-weight: bold; 72 | } 73 | 74 | .button{ 75 | /* width: 35px; */ 76 | width: 40%; 77 | height: 17px; 78 | background-color: #f1f5f8; 79 | margin-left: 2px; 80 | 81 | } 82 | 83 | .buttonContainer { 84 | display: none; 85 | position: absolute; 86 | width: 5%; 87 | margin-left: 8px; 88 | } 89 | 90 | #quantity:hover > .buttonContainer { 91 | display: inline-block; 92 | } 93 | 94 | #quantity { 95 | display: inline-block; 96 | } 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /inventorydemo/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | 9 | root.render ( 10 | 11 | ) -------------------------------------------------------------------------------- /inventorydemo/client/services/events.js: -------------------------------------------------------------------------------- 1 | import { changeSingleInventoryField, getSingleInventory } from './inventory' 2 | 3 | //helper functions for events 4 | 5 | const handleIncremementClickHelper = async (id, field, value) => { 6 | //wait for result of calling function to increment DB 7 | await changeSingleInventoryField(id, field, value) 8 | //then call the fucntion that gets inventory of single item from DB (update) 9 | // return getSingleInventory(id) 10 | } 11 | 12 | const handleDecrementClickHelper = async (id, field, value) => { 13 | //wait for result of calling function to decrement DB 14 | return changeSingleInventoryField(id, field, value) 15 | //then call the fucntion that gets inventory of single item from DB (update) 16 | // return getSingleInventory(id) 17 | } 18 | 19 | export { 20 | handleIncremementClickHelper, 21 | handleDecrementClickHelper, 22 | } 23 | -------------------------------------------------------------------------------- /inventorydemo/client/services/inventory.js: -------------------------------------------------------------------------------- 1 | //helper functions for all requests to inventory router 2 | module.exports = inventoryFunctions = { 3 | 4 | getAllInventory: () => { 5 | return fetch('/inventory/all') 6 | .then(data => data.json()) 7 | }, 8 | 9 | getSingleInventory: (id) => { 10 | return fetch('/inventory/getOne/' + id) 11 | .then(data => data.json()) 12 | }, 13 | 14 | createInventory: (item, quantity, description, price) => { 15 | return fetch('/inventory/create', { 16 | method: 'POST', 17 | headers: { 'Content-Type': 'application/json' }, 18 | body: JSON.stringify({ 19 | item: item, 20 | quantity: quantity, 21 | description: description, 22 | price: price, 23 | }) 24 | }); 25 | }, 26 | 27 | changeSingleInventoryField: (id, field, value) => { 28 | return fetch('/inventory/changeSingleField', { 29 | method: 'PATCH', 30 | headers: { 'Content-Type': 'application/json' }, 31 | body: JSON.stringify({ 32 | id: id, 33 | field: field, 34 | value: value, 35 | }) 36 | }) 37 | } 38 | } -------------------------------------------------------------------------------- /inventorydemo/public/images/50.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/50.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/Free.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/Free.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/Portrait.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/Portrait.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/beautiful life.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/beautiful life.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/body & arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/body & arms.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/hold me in your arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/hold me in your arms.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/keep it turned on.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/keep it turned on.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/logo.png -------------------------------------------------------------------------------- /inventorydemo/public/images/my red book.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/my red book.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/transparent.png -------------------------------------------------------------------------------- /inventorydemo/public/images/whenever you need.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/inventorydemo/public/images/whenever you need.jpeg -------------------------------------------------------------------------------- /inventorydemo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | inventory Demo 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /libraries/clientlib/README.md: -------------------------------------------------------------------------------- 1 | # Using the hook useSubscribe( ) 2 | 3 | First import the module into the file you wish to use it. 4 | 5 | ```js 6 | import { useSubscribe } from '@livestatedb/client'; 7 | ``` 8 | 9 | When useSubscribe is called, it will open a web socket through which database updates get passed when changes occur that state is 'subscribed to'. 10 | 11 | useSubscribe takes one argument: an object with three key-value pairs that correspond with the database name, collection name, and query to a database. Here's an example. 12 | 13 | ```js 14 | const options = { 15 | database: 'DBname', 16 | collection: 'someCollectionName', 17 | query: {} 18 | }; 19 | ``` 20 | 21 | useSubscribe returns the way to reference state in your front-end code, and a function that ends that piece of state's subscription to the database. 22 | 23 | ```js 24 | const [ state, endSubscription ] = useSubscribe(options); 25 | ``` 26 | 27 | _**NOTE!**_
28 | useSubscribe will only receive updates from the database that match the options passed-in _at the time it was called_. If a database change occurs that would match the query parameters, it will not automatically be included in what useSubscribe is subscribed to. You would need to re-render the page to call useSubscribe again to capture this. 29 | 30 | Additionally, your query filter parameters should be constant. For example, if you are building a dashboard for managing inventory, your filter parameters could include condition: (new, like new, fair, poor), but should not include parameters such as quantity: (>50, <=50). In this example, if you wanted to be subscribed to data that had a certain quantity, you would need write your options object, and specifically your query parameters, to include all of the data you're interested in, regardless of quantity. You would then be able to filter out only the data you're interested in in your front-end codebase. -------------------------------------------------------------------------------- /libraries/clientlib/customHook.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo, useRef } from 'react'; 2 | import uuid from 'react-uuid'; 3 | import { io } from 'socket.io-client' 4 | 5 | const Subscribe = () => { 6 | const socket = io("/", { 7 | path: '/websocket', 8 | transports: ["websocket"] 9 | }) 10 | const setup = (params) => socket.emit('setup', params); 11 | 12 | const useSubscribe = ({ database, collection, query }) => { 13 | const [ state, setstate ] = useState({}); 14 | const subscriptionId = useMemo(() => uuid(), []) 15 | const stringifiedQuery = JSON.stringify(query) 16 | const currSocket = useRef(null); 17 | const didMount = useRef(false); 18 | 19 | useEffect(() => { 20 | const params = { 21 | database: database, 22 | collection: collection, 23 | query: stringifiedQuery, 24 | subscriptionId: subscriptionId 25 | } 26 | 27 | setup(params); 28 | // socket.emit('setup', params) 29 | currSocket.current = socket; 30 | 31 | socket.on(subscriptionId, (e) => { 32 | const {type, data} = JSON.parse(e); 33 | const id = data.documentKey?._id; 34 | switch (type) { 35 | case 'get': 36 | { 37 | const obj = {}; 38 | for(let i = 0; i < data.length; i++){ 39 | obj[data[i]._id] = data[i]; 40 | // delete obj[data[i]._id]._id; 41 | } 42 | setstate(obj); 43 | break; 44 | } 45 | case 'insert' : 46 | { 47 | const newInsertion = {}; 48 | newInsertion[id] = data.fullDocument; 49 | setstate((previousstate) => 50 | { 51 | const updatedstate = JSON.parse(JSON.stringify(previousstate)); 52 | Object.assign(updatedstate, newInsertion); 53 | return updatedstate; 54 | }); 55 | break; 56 | } 57 | case 'update' : 58 | { 59 | const update = data.updateDescription.updatedFields; 60 | setstate((previousstate) => 61 | { 62 | const updatedstate = JSON.parse(JSON.stringify(previousstate)); 63 | Object.assign(updatedstate[id], update); 64 | return updatedstate; 65 | }); 66 | break; 67 | } 68 | case 'delete': 69 | { 70 | setstate((previousstate) => 71 | { 72 | const updatedstate = JSON.parse(JSON.stringify(previousstate)); 73 | delete updatedstate[id]; 74 | return updatedstate; 75 | }); 76 | break; 77 | } 78 | } 79 | }) 80 | 81 | return () => { 82 | // Unsubscribe from event stream 83 | endSubscription(); 84 | } 85 | 86 | 87 | }, []); 88 | 89 | useEffect(() => { 90 | if (didMount.current) { 91 | currSocket.current.emit('depChange', { 92 | database: database, 93 | collection: collection, 94 | query: stringifiedQuery, 95 | subscriptionId: subscriptionId 96 | }) 97 | //event emitter for letting the server know something has changed for subscription 98 | }else didMount.current = true; 99 | }, [database, collection, stringifiedQuery]) 100 | 101 | //write function that stops subscriptions 102 | const endSubscription = async () => { 103 | await currSocket.current.close(); 104 | } 105 | 106 | return [ state, endSubscription ]; 107 | } 108 | return useSubscribe; 109 | } 110 | 111 | export const useSubscribe = Subscribe(); 112 | -------------------------------------------------------------------------------- /libraries/clientlib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livestatedb/client", 3 | "version": "1.0.0", 4 | "description": "React custom hook for client connection to livestate API", 5 | "main": "customHook.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@livestatedb/client": "^1.0.0", 13 | "react": "^18.2.0", 14 | "react-uuid": "^1.0.3", 15 | "socket.io-client": "^4.5.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libraries/serverlib/README.md: -------------------------------------------------------------------------------- 1 | # Using the back-end library 2 | 3 | useSubscribe won't work until you pass information about your MongoDB and Redis into a function that opens a web socket and connects everything. Require the module in wherever you instantiate an http server, for example server.js. Feel free to give it a label or to to simply invoke the required-in function with the necessary arguments. 4 | 5 | ```js 6 | const liveStateDB = require('@livestatedb/server'); 7 | liveStateDB(httpServer, databaseInfo); 8 | ``` 9 | OR 10 | ```js 11 | require('@livestatedb/server')(httpServer, databaseInfo); 12 | ``` 13 | 14 | The second paramater, databaseInfo, must be an object with two key-value pairs - these are for passing in information about your MongoDB database and Redis, respectively. The values are both objects that contain connection information for each database. See example below. 15 | 16 | ```js 17 | const databaseInfo = 18 | { 19 | mongoDbOptions: 20 | { 21 | uri: "mongodb+srv://name:12345678910abcde@cluster0.aabbcc.mongodb.net/?retryWrites=true&w=majority" 22 | }, 23 | redisDbOptions: 24 | { 25 | host: 'redis-00000.c00.us-east-0-0.ec2.cloud.redislabs.com', 26 | port: 15711, 27 | password: 'lkajsdf092j3jlsdfmop3jfspdkgpoi', 28 | family: 4 29 | }, 30 | } 31 | ``` -------------------------------------------------------------------------------- /libraries/serverlib/database/databaseConnect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const Redis = require('ioredis') 5 | const { MongoClient } = require('mongodb'); 6 | 7 | module.exports = async (changeStreamOptions) => { 8 | const { mongoDbOptions, redisDbOptions } = changeStreamOptions; 9 | 10 | try { 11 | const client = new MongoClient(mongoDbOptions.uri); 12 | await client.connect(); 13 | const redis = new Redis(redisDbOptions); 14 | 15 | 16 | return {redis, client} 17 | } catch (err) { 18 | 19 | if (err) { 20 | console.log(`Error occured when connecting to MongoDB or Redis. errName: ${err.name}, errMessage: ${err.message}, errStack: ${err.stack}`) 21 | 22 | } else { 23 | let dbError = new Error('An unknown error occured when connecting to MongoDB or Redis') 24 | console.log(dbError) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /libraries/serverlib/events/eventHelperFuncs.js: -------------------------------------------------------------------------------- 1 | // * REDIS LEGEND - the strings below are added to the begining of the doc to create unique ID's 2 | // * SD = Subscription based on Document 3 | // * SC = Subscription based on Subcription ID's 4 | // * SQD = Subscription based on Query 5 | // * SQS = Subscription Query String 6 | 7 | //! File originally written without const declaration, but didn't error -> WHY? 8 | const eventRouteHelperFuncs = {} 9 | 10 | //find documents that are querried 11 | eventRouteHelperFuncs.initialDbQuery = async (dbCollection, query, redis, subscriptionId, io, websocketObj) => { 12 | try { 13 | const data = await dbCollection.find(JSON.parse(query)).toArray() 14 | //iterate through the array of objects from the db 15 | for(let objs of data){ 16 | //check if the object's id has an entry in the subscription db 17 | //'SD'+ 18 | const redisClientIsSubscribedToDocument = await redis.sismember('SD' + objs._id, subscriptionId); 19 | if(redisClientIsSubscribedToDocument === 0){ 20 | await redis.sadd('SD' + objs._id, [subscriptionId]); 21 | } 22 | //check if the document is in the clients subscriptions 23 | const redisDocumentIsSubscribedToClient = await redis.sismember('SC' + subscriptionId, objs._id); 24 | if(redisDocumentIsSubscribedToClient === 0){ 25 | await redis.sadd('SC' + subscriptionId, [objs._id]); 26 | } 27 | } 28 | io.to(websocketObj[subscriptionId]).emit(subscriptionId, JSON.stringify({type: 'get', data: data})) 29 | } catch (err) { 30 | if (err) { 31 | console.log(`Error occured while subscribing or unsubscribing to the DB query. errName: ${err.name}, errMessage: ${err.message}, errStack: ${err.stack}`) 32 | } else { 33 | let initialDbQueryError = new Error('An unknown error occured while subscribing or unsubscribing to the DB query') 34 | console.log(initialDbQueryError) 35 | } 36 | } 37 | } 38 | 39 | //write helper function here for checking our redis databases for subscribed clients to send response to 40 | const sendReplyToSubscribers = async (setOfSubscriptionIds, redis, changeStreamObj, websocketObj, io) => { 41 | for(const subscriptionId of setOfSubscriptionIds) { 42 | try { 43 | // if we fail to write to a client we want to mark the client for removal 44 | io.to(websocketObj[subscriptionId]).emit(subscriptionId, JSON.stringify({type: changeStreamObj.operationType, data: changeStreamObj})) 45 | } catch (err) { 46 | if (err) { 47 | console.log(`Error occured in sending reply to subscribers. errName: ${err.name}, errMessage: ${err.message}, errStack: ${err.stack}`) 48 | } else { 49 | let subscriberReplyError = new Error('An unknown error occured in sending reply to subscribers') 50 | console.log(subscriberReplyError) 51 | } 52 | } 53 | } 54 | } 55 | 56 | eventRouteHelperFuncs.monitorListingsUsingEventEmitter = async (client, redis, websocketObj, io, timeInMs = 2147483647, pipeline = []) => { 57 | const changeStream = client.watch(pipeline); 58 | //listen for changes 59 | changeStream.on('change', async (changeStreamObj) => { 60 | //check if it's an insertion event 61 | if(changeStreamObj.operationType === 'insert'){ 62 | try { 63 | const { db, coll } = changeStreamObj.ns; 64 | // store set of Subscription Ids for collection 65 | const redisSubscriptionIdsSubscribedToCollection = await redis.smembers('DB' + db + 'COL' + coll); 66 | // iterate through subscription IDs 67 | for(let subscriptionId of redisSubscriptionIdsSubscribedToCollection){ 68 | // for each subscription ID check to see if it is in the set stored at subscribed Doc 69 | const redisClientIsSubscribedToDocument = await redis.sismember('SD' + changeStreamObj.documentKey._id.toString(), subscriptionId); 70 | // if not subscribed 71 | if(redisClientIsSubscribedToDocument === 0){ 72 | // add subscription ID to doc ID key 73 | await redis.sadd('SD' + changeStreamObj.documentKey._id.toString(), [subscriptionId]); 74 | } 75 | // store set of Documents subscribed to Client 76 | const redisDocumentIsSubscribedToClient = await redis.sismember('SC' + subscriptionId, changeStreamObj.documentKey._id.toString()); 77 | // if not subscribed 78 | if(redisDocumentIsSubscribedToClient === 0){ 79 | // add docID to subscription ID key 80 | await redis.sadd('SC' + subscriptionId, [changeStreamObj.documentKey._id.toString()]); 81 | } 82 | } 83 | sendReplyToSubscribers(redisSubscriptionIdsSubscribedToCollection, redis, changeStreamObj, websocketObj, io); 84 | } catch (err) { 85 | if (err) { 86 | console.log(`Unable to save new subscriptions in Redis. errName: ${err.name}, errMessage: ${err.message}, errStack: ${err.stack}`) 87 | 88 | } else { 89 | let initialDbQueryError = new Error('An unknown error occured while attempting to save new subscriptions in Redis') 90 | console.log(initialDbQueryError) 91 | } 92 | } 93 | } 94 | try { 95 | // store all subscribers for a particular document 96 | const redisSubscriptionIdsSubscribedToDocument = await redis.smembers('SD' + changeStreamObj.documentKey._id.toString()); 97 | sendReplyToSubscribers(redisSubscriptionIdsSubscribedToDocument, redis, changeStreamObj, websocketObj, io); 98 | } catch (err) { 99 | if (err) { 100 | console.log(`Unable to access subscriptions in Redis. errName: ${err.name}, errMessage: ${err.message}, errStack: ${err.stack}`) 101 | 102 | } else { 103 | let initialDbQueryError = new Error('An unknown error occured while attempting to access subscriptions in Redis') 104 | console.log(initialDbQueryError) 105 | } 106 | } 107 | }); 108 | await closeChangeStream(timeInMs, changeStream); 109 | } 110 | 111 | eventRouteHelperFuncs.unsubscribe = async(redis, websocketId, websocketObj, subscriptionIdObj, changeStreams) => { 112 | for(const subscriptionId of subscriptionIdObj[websocketId]){ 113 | const docs = await redis.smembers('SC' + subscriptionId) 114 | for(const doc of docs){ 115 | redis.srem('SD' + doc, subscriptionId) 116 | } 117 | const changeStreams = await redis.smembers('DBCOL' + websocketId) 118 | for(const changeStream of changeStreams){ 119 | await redis.srem(changeStream, websocketId) 120 | } 121 | redis.del('DBCOL' + websocketId); 122 | redis.del('SC' + subscriptionId); 123 | } 124 | } 125 | 126 | eventRouteHelperFuncs.changeSubscribe = async(redis, websocketId, websocketObj) => { 127 | 128 | } 129 | 130 | //this function will close the stream after a specified amount of time 131 | function closeChangeStream(timeInMs, changeStream) { 132 | return new Promise((resolve) => { 133 | setTimeout(() => { 134 | console.log("Closing the change stream"); 135 | changeStream.close(); 136 | resolve(); 137 | }, timeInMs) 138 | }) 139 | } 140 | 141 | module.exports = eventRouteHelperFuncs; -------------------------------------------------------------------------------- /libraries/serverlib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livestatedb/server", 3 | "version": "1.0.1", 4 | "description": "LivestateDB API server package that will hook into an http server", 5 | "main": "setupWebsocket.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ioredis": "^5.2.3", 13 | "mongodb": "^4.10.0", 14 | "socket.io": "^4.5.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libraries/serverlib/setupWebsocket.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { initialDbQuery, monitorListingsUsingEventEmitter, unsubscribe } = require('./events/eventHelperFuncs') 4 | //holds current changeStream's open by DB & Collection 5 | const changeStreams = {}; 6 | //keeps track of corresponding clients and their respective response objects 7 | const websocketObj = {}; 8 | const subscriptionIdObj = {}; 9 | 10 | const changeStreamOptionsType = { 11 | database: 'string', 12 | collection: 'string', 13 | query: 'string', 14 | subscriptionId: 'string' 15 | } 16 | 17 | module.exports = async (server, changeStreamOptions) => { 18 | 19 | const {redis, client} = await require('./database/databaseConnect')(changeStreamOptions); 20 | const io = require('socket.io')(server, { 21 | path: '/websocket', 22 | transports: ["websocket"], 23 | }) 24 | io.on('connection', async (socket) => { 25 | socket.on('setup', async (changeStreamOptions) => { 26 | for (let prop in changeStreamOptions) { 27 | if (typeof changeStreamOptions[prop] !== changeStreamOptionsType[prop]) { 28 | throw new Error(`\x1b[36m${prop} changeStreamOptions should be ${changeStreamOptionsType[prop]}\x1b[0m`) 29 | } 30 | } 31 | try { 32 | const { subscriptionId, collection, database, query } = changeStreamOptions; 33 | //keep track of connection/reply object by clientsubscriptionId 34 | if(!websocketObj[subscriptionId]) websocketObj[subscriptionId] = socket.id; 35 | //!! can use redis for this later 36 | (subscriptionIdObj[socket.id]) ? 37 | subscriptionIdObj[socket.id].add(subscriptionId) : 38 | subscriptionIdObj[socket.id] = new Set([subscriptionId]) 39 | //connect to the current collection 40 | const dbCollection = client.db(database).collection(collection); 41 | 42 | //add add clients to the DB & Collection's they are subscribed too -- for 'insert' updates 43 | const subscriptionDbCollectionKeyDbString = 'DB' + database + 'COL' + collection; 44 | const redisClientIsSubscribedToCollection = await redis.sismember(subscriptionDbCollectionKeyDbString, subscriptionId); 45 | if(redisClientIsSubscribedToCollection === 0){ 46 | await redis.sadd(subscriptionDbCollectionKeyDbString, [socket.id]) 47 | } 48 | await redis.sadd('DBCOL' + socket.id, [subscriptionDbCollectionKeyDbString]) 49 | initialDbQuery(dbCollection, query, redis, subscriptionId, io, websocketObj); 50 | 51 | //check if there is already a changestream for the current collection 52 | if(!changeStreams[database]?.has(collection)) { 53 | //if database exists in object, add collection to set, if not make an entry with key database equal to new set with collection 54 | (changeStreams[database]) ? changeStreams[database].add(collection) : changeStreams[database] = new Set([collection]) 55 | await monitorListingsUsingEventEmitter(dbCollection, redis, websocketObj, io); 56 | } 57 | } catch (err) { 58 | if (err) { 59 | if (err) { 60 | console.error('setupWebsocket err:', err) 61 | } else { 62 | let initialDbQueryError = new Error('An unknown error occured while setting up the websocket') 63 | console.error(initialDbQueryError) 64 | } 65 | } 66 | } 67 | }) 68 | socket.on('depChange', async ({database, collection, query, subscriptionId}) => { 69 | const currChangeStreamSub = await redis.smembers('DBCOL' + socket.id); 70 | if('DB' + database + 'COL' + collection != currChangeStreamSub){ 71 | //check to make sure the current open changestream is needed 72 | 73 | } 74 | const dbCollection = client.db(database).collection(collection); 75 | //check if the new database/collection have a change streams open 76 | await unsubscribe(redis, socket.id, websocketObj, subscriptionIdObj, changeStreams); 77 | //call a function that unsubscribes from all docs for the current subscriptionId 78 | //add add clients to the DB & Collection's they are subscribed too -- for 'insert' updates 79 | const subscriptionDbCollectionKeyDbString = 'DB' + database + 'COL' + collection; 80 | await redis.sadd(subscriptionDbCollectionKeyDbString, [socket.id]) 81 | await redis.sadd('DBCOL' + socket.id, [subscriptionDbCollectionKeyDbString]) 82 | initialDbQuery(dbCollection, query, redis, subscriptionId, io, websocketObj) 83 | }) 84 | socket.on('disconnect', () => { 85 | unsubscribe(redis, socket.id, websocketObj, subscriptionIdObj, changeStreams); 86 | }) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livestate", 3 | "version": "1.0.0", 4 | "description": "A real time state management library that allows for state to subscribe and update based on changes from a specific portion of a database.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon server/server.js & NODE_ENV=development webpack serve --open", 8 | "build": "NODE_ENV=production webpack", 9 | "watch": "webpack --watch", 10 | "dev": "NODE_ENV=development webpack serve --open" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "build", 15 | "client", 16 | "__tests__" 17 | ] 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/seadubs/nerm-template.git" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/seadubs/nerm-template/issues" 27 | }, 28 | "homepage": "https://github.com/seadubs/nerm-template#readme", 29 | "dependencies": { 30 | "@livestatedb/client": "^1.0.0", 31 | "express": "^4.18.1", 32 | "fastify-sse": "^1.0.0", 33 | "fastify-sse-v2": "^2.2.1", 34 | "mongodb": "^4.10.0", 35 | "mongoose": "^6.4.7", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "react-icons": "^4.6.0", 39 | "react-redux": "^8.0.2", 40 | "react-uuid": "^1.0.3", 41 | "socket.io-client": "^4.5.3" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.18.9", 45 | "@babel/preset-env": "^7.18.9", 46 | "@babel/preset-react": "^7.18.6", 47 | "@fastify/cors": "^8.1.0", 48 | "@fastify/redis": "^6.0.1", 49 | "@livestatedb/server": "^1.0.1", 50 | "babel-jest": "^29.2.1", 51 | "babel-loader": "^8.2.5", 52 | "cors": "^2.8.5", 53 | "css-loader": "^6.7.1", 54 | "fastify": "^4.8.1", 55 | "html-webpack-plugin": "^5.5.0", 56 | "ioredis": "^5.2.3", 57 | "jest": "^29.2.1", 58 | "nodemon": "^2.0.19", 59 | "redux-devtools-extension": "^2.13.9", 60 | "sass": "^1.54.0", 61 | "sass-loader": "^13.0.2", 62 | "socket.io": "^4.5.3", 63 | "spdy": "^4.0.2", 64 | "style-loader": "^3.3.1", 65 | "webpack": "^5.74.0", 66 | "webpack-cli": "^4.10.0", 67 | "webpack-dev-server": "^4.9.3" 68 | }, 69 | "keywords": [] 70 | } 71 | -------------------------------------------------------------------------------- /server/controllers/inventoryController.js: -------------------------------------------------------------------------------- 1 | const Inventory = require('../models/inventoryModel'); 2 | const { createErr } = require('../utils/utils'); 3 | 4 | const inventoryController = {}; 5 | 6 | // fetches all inventory from the db 7 | inventoryController.getAll = async (req, res, next) => { 8 | try { 9 | const dbRes = await Inventory.find({}); 10 | res.locals.inventory = dbRes; 11 | } catch (err) { 12 | return next( 13 | createErr({ 14 | method: 'getAllInventory', 15 | type: 'db query error', 16 | err, 17 | }) 18 | ); 19 | } 20 | 21 | return next(); 22 | }; 23 | 24 | // creates a new entry in the db 25 | inventoryController.create = async (req, res, next) => { 26 | const required = ['item', 'quantity', 'description', 'price']; 27 | const { item, quantity, description, price } = req.body; 28 | 29 | if (required.some((key) => req.body[key] === undefined)) { 30 | return next( 31 | createErr({ 32 | method: 'createInventoryItem', 33 | type: 'data validation error', 34 | err: 'request body did not include all required fields', 35 | }) 36 | ); 37 | } 38 | 39 | if ( 40 | typeof item !== 'string' || 41 | typeof quantity !== 'number' || 42 | typeof price !== 'number' || 43 | typeof description !== 'string' 44 | ) { 45 | return next( 46 | createErr({ 47 | method: 'createInventoryItem', 48 | type: 'data validation error', 49 | err: 'request body contained invalid data', 50 | }) 51 | ); 52 | } 53 | 54 | try { 55 | const dbRes = await Inventory.create( 56 | { item: item, 57 | quantity: quantity, 58 | price: price, 59 | description: description, 60 | }); 61 | res.locals.newExample = dbRes; 62 | } catch (err) { 63 | return next( 64 | createErr({ 65 | method: 'createExample', 66 | type: 'db insert error', 67 | err, 68 | }) 69 | ); 70 | } 71 | 72 | return next(); 73 | }; 74 | 75 | //changes a single field in the db only works for incrementing and decrementing 76 | inventoryController.changeSingleField = async (req, res, next) => { 77 | const required = ['id', 'field', 'value']; 78 | const { id, field, value } = req.body; 79 | 80 | if (required.some((key) => req.body[key] === undefined)) { 81 | return next( 82 | createErr({ 83 | method: 'changeSingleField', 84 | type: 'data validation error', 85 | err: 'request body did not include all required fields', 86 | }) 87 | ); 88 | } 89 | 90 | if ( 91 | typeof id !== 'string' || 92 | typeof field !== 'string' || 93 | typeof value !== 'number' 94 | ) { 95 | return next( 96 | createErr({ 97 | method: 'changeSingleField', 98 | type: 'data validation error', 99 | err: 'request body contained invalid data', 100 | }) 101 | ); 102 | } 103 | 104 | try { 105 | const updateObj = {} 106 | updateObj.$inc = {}; 107 | updateObj.$inc[field] = value; 108 | 109 | const dbRes = await Inventory.updateOne( 110 | { _id: id }, 111 | updateObj); 112 | res.locals.updatedInventory = dbRes; 113 | } catch (err) { 114 | return next( 115 | createErr({ 116 | method: 'changeSingleField', 117 | type: 'db insert error', 118 | err, 119 | }) 120 | ); 121 | } 122 | 123 | return next(); 124 | }; 125 | 126 | //gets a single entry from the db 127 | inventoryController.getOne = async (req, res, next) => { 128 | const { id } = req.params; 129 | try { 130 | const dbRes = await Inventory.find({ _id: id}); 131 | res.locals.inventory = dbRes; 132 | } catch (err) { 133 | return next( 134 | createErr({ 135 | method: 'getOne', 136 | type: 'db query error', 137 | err, 138 | }) 139 | ); 140 | } 141 | return next(); 142 | }; 143 | 144 | 145 | module.exports = inventoryController; 146 | -------------------------------------------------------------------------------- /server/keys/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDNDCCAhwCCQCvxf5F8fs6kTANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV 3 | UzERMA8GA1UECAwITmV3IFlvcmsxEjAQBgNVBAcMCVBhdGNob2d1ZTESMBAGA1UE 4 | CgwJTGl2ZVN0YXRlMRIwEAYDVQQLDAlMaXZlU3RhdGUwHhcNMjIxMDA2MDMyNzQ0 5 | WhcNMjMxMDA2MDMyNzQ0WjBcMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlv 6 | cmsxEjAQBgNVBAcMCVBhdGNob2d1ZTESMBAGA1UECgwJTGl2ZVN0YXRlMRIwEAYD 7 | VQQLDAlMaXZlU3RhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2 8 | 6BREcYUeFWgfH0NhPVqRne+x8LsWPabo633uXXT+V3l0hqcchzKYlLHbxiOLJ8QY 9 | noc/ocUEXN2+hN7e3ncxMz6Pdn/+Q7nqM9WqwaBMQz3gULVDgK2mV86bZElJbH92 10 | eicij6BsSCEYwSe/k9WNikT85nlrN1D9lQP4vOpy5xnhKrb1X4Gn3jzaGugGpsq7 11 | Kbh9N25x8Hu37sWQGu3WYwX218ZlqQIAEP98R/WdiGDS1vEdrBak+cCo7sNL1Pta 12 | wtQvbaUVgETOSgiMWtOoaHBDTtAbsx/p+5U9ImTr/aMbF4OfmIENhTY71kalTTZj 13 | Pg9yi1piv5lcBikNpsflAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJH/rxrh3pXj 14 | QEBXa88oZocv3BwYJ3F8O5x7+1BTz2S7kdUXOlGpDHjZ0cZSO2ZwCFvMVj0c/CsP 15 | iasvDASoOBcQJ8FYwrUxKqeGoVCsoDHCPsVG96u/vnRUSBhRc+uyVuyqPxHOHyPy 16 | BnEmYeYW4cUPHWma+hana5BzSIH2wE3yVyLvMA9/AqKVy8/2gme/Z4Bo6kuq48p/ 17 | moaBkRNLxPlM5BBLdu9JtCyFboNP8uuLQjRFguk6xxfHyLWVT7W8pcPyt7Qp+hPv 18 | FVygpINWTlEA7uELgaQr/BfXtIBy+RqI4soD1Aw62JH8JtS+Udd59rFa/I7gfCgN 19 | eu/W0Q55I4o= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /server/keys/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC26BREcYUeFWgf 3 | H0NhPVqRne+x8LsWPabo633uXXT+V3l0hqcchzKYlLHbxiOLJ8QYnoc/ocUEXN2+ 4 | hN7e3ncxMz6Pdn/+Q7nqM9WqwaBMQz3gULVDgK2mV86bZElJbH92eicij6BsSCEY 5 | wSe/k9WNikT85nlrN1D9lQP4vOpy5xnhKrb1X4Gn3jzaGugGpsq7Kbh9N25x8Hu3 6 | 7sWQGu3WYwX218ZlqQIAEP98R/WdiGDS1vEdrBak+cCo7sNL1PtawtQvbaUVgETO 7 | SgiMWtOoaHBDTtAbsx/p+5U9ImTr/aMbF4OfmIENhTY71kalTTZjPg9yi1piv5lc 8 | BikNpsflAgMBAAECggEBAKHSRkwfm0R4b/xI7pYTcBOxBaYKc5tDxG4CGxJOwPld 9 | buk3UJYezxbLR2hHrp6o+gdGCiaz+tjXSKAMchn//LgcvFa04pyUUuHwJjPCxw1x 10 | 2EajYRtttzSaLwKwAV7ijQrHG+Sp6aajUhTSn1YRdziYgly1k+rVr+QKeM8SZOJu 11 | MJpkvcWbFnjMpt6TggOt8HK4hk5HzQQ9tIULpFmkYuuIhURuHmPKwxABuyQvz+Qs 12 | z+J3yBF5oDOS1o1kfgX1zOZBdORkyShZ4nP0+xUaE7VgHx6Zwqd3azBAX1ynDJ2z 13 | iGn9MEy/jURXf5rUPL+qlWQolC8rTVnxz59CH9DTpAECgYEA5YXQys/csQ9mx7oD 14 | ckHxzwamCgFeado46f47etzDennZjlp4n/fs3Zyn3e7PFNl56Eu0/6Ne6ty4Bijy 15 | 67Qc1wyGyGkLSLZSViMBElA6ZdGFsTGWFLmb5ieavjDeaeV7FK39Hn3jFBHSgcjL 16 | asQH2UWsIbaXEy1DV0YSp12b14ECgYEAzAGcjiwzWbIa3Tq/rNaAEkOCJj72T60+ 17 | AfJa4p7jJxpmelpVFrhLCh+gJdOtiq3WGQWwt+zJuWXSDQJt98b2W0PrTiTzGcb7 18 | 8LWngOovWUam5GjUMpQNaYhmebl4eKRwkYcZ8YQ4cPLPtnYUq9fp68ioYfXHB34c 19 | FIQ7+OtbwmUCgYBGMNgbTdoF49ngtogbRwN19miMosKGyI+jL3ZtKlo1eFIJ8Kz2 20 | 7tDnLONBQajejWt0mMJvczyxwnIcoCU548j2EwSdAVLMF8WyRd4sBZD2Vk72U128 21 | VM7SYcY256b854ruVg8UZhctqg2gVxGuQujLjz0GyloFKVBwviMyP7RLAQKBgCgt 22 | bRLG+7n/jLDjQBqfCAwQxZIYtlPMz7h8bY/SMPRji6kvHzDhTM4KmnS4sqXsRI0b 23 | BMnEVZowxmSPhO7WfXBN+Qqj1kjOWpH2hK2r8XVIp9e2GYx4gge1uFDiySohYJYY 24 | oHBjVOZRu7y4lqudU1F/bEIWsIL2QDt+K32DhMfNAoGASJjL+uCNaaXbAGw9dEZp 25 | JOHuoIiQ3eD4X/JAgZe/1kG3iLHlDWMFNKCjeWwntWhdUWUFpuS8jFkaNvRVNjDZ 26 | 47IGZ9wPWnP8lOmJa+H/bYMpKX2+3W9Pd8Z3tbd9kgN+xRBUB5oSQ+wo3g12CkL4 27 | 1ZKTYnkJ++i7iGLK8C2ijT0= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /server/models/inventoryModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | //Kevin's MongoDB (I did a few things to make Change Stream work -- only works with sharded online URI...local for current dev is fine) 6 | const MONGO_URI = ''; 7 | // const MONGO_URI = 'mongodb://localhost:27017'; 8 | 9 | mongoose.connect(MONGO_URI, { 10 | // options for the connect method to parse the URI 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | // sets the name of the DB that our collections are part of 14 | dbName: 'inventoryDemo' 15 | }) 16 | .then(() => console.log('Mongoose connected to Mongo DB.')) 17 | .catch(err => console.log(err)); 18 | 19 | const Schema = mongoose.Schema; 20 | 21 | // inventory tracker using just item name and quantity 22 | const inventorySchema = new mongoose.Schema({ 23 | item: { type: String, required: true}, 24 | quantity: { type: Number, default: 0 }, 25 | description: { type: String, require: true}, 26 | price: { type: Number, required: true} 27 | }); 28 | 29 | const inventory = mongoose.model('inventoryItem', inventorySchema); 30 | 31 | // You must export your model through module.exports 32 | // The collection name should be 'student' 33 | module.exports = inventory; 34 | -------------------------------------------------------------------------------- /server/routes/inventory.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const inventoryController = require('../controllers/inventoryController') 3 | 4 | const router = Router(); 5 | 6 | 7 | router.get('/all', inventoryController.getAll, (req, res) => { 8 | res.status(200).json(res.locals.inventory); 9 | }); 10 | 11 | router.get('/getOne/:id', inventoryController.getOne, (req, res) => { 12 | res.status(200).json(res.locals.inventory); 13 | }); 14 | 15 | router.post('/create', inventoryController.create, (req, res) => { 16 | res.status(200).json({message: 'created a new entry'}); 17 | }) 18 | 19 | router.patch('/changeSingleField', inventoryController.changeSingleField, (req, res) => { 20 | res.status(200).json(res.locals.updatedInventory); 21 | }) 22 | 23 | //general handeler if request not found/handled 24 | router.use((req, res) => { 25 | console.log(`server/routes/api.js: handler not found for request ${req.method} ${req.url}`); 26 | res 27 | .status(404) 28 | .json({ 29 | message: `Inventory handler for ${req.method} ${req.url} not found`, 30 | }); 31 | }); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const inventory = require('./routes/inventory'); 4 | const fs = require('fs'); 5 | const http = require('http'); 6 | 7 | 8 | const PORT = process.env.EXPRESS_PORT || 3000; 9 | const app = express(); 10 | 11 | app.use(express.json()); 12 | 13 | const corsOptions = { 14 | origin: true, 15 | credentials: true, 16 | }; 17 | const cors = require('cors')(corsOptions); 18 | 19 | // Serve the client build 20 | app.use('/build', express.static(path.resolve(__dirname, '../storedemo/build'))); 21 | 22 | // Handle router calls 23 | app.use('/inventory', inventory); 24 | 25 | // Serve index.html 26 | app.get('/', (req, res) => { 27 | res.sendFile(path.resolve(__dirname, '../storedemo/public/', 'index.html')); 28 | }); 29 | 30 | // Default 404 handler 31 | app.use((req, res) => { 32 | res 33 | .status(404) 34 | .send( 35 | 'Page not found' 36 | ); 37 | }); 38 | 39 | // Global error handler 40 | app.use((err, req, res, next) => { 41 | const defaultErr = { 42 | message: { err: 'An error occurred' }, 43 | log: 'Express error handler caught unknown middleware error', 44 | status: 400, 45 | }; 46 | const errObj = Object.assign(defaultErr, err); 47 | console.log('ErrorObject Log: ', errObj.log); 48 | res.status(errObj.status).send(errObj.message); 49 | }); 50 | 51 | const server = http.Server(app); 52 | server.listen(3000, () => console.log('listening on port 3000')); 53 | 54 | const changeStreamOptions = 55 | { 56 | // mongoDbOptions: 57 | // { 58 | // uri: "" 59 | // }, 60 | // redisDbOptions: 61 | // { host: , 62 | // port: , 63 | // password: , 64 | // family: 65 | // }, 66 | } 67 | 68 | require('../libraries/serverlib/setupWebsocket')(server, changeStreamOptions) 69 | .catch(console.error) 70 | -------------------------------------------------------------------------------- /server/utils/utils.js: -------------------------------------------------------------------------------- 1 | const {basename} = require('path') 2 | 3 | const utils = { 4 | createErr: (errInfo) => { 5 | const { method, type, err } = errInfo; 6 | return { 7 | log: `${basename(__filename)}.${method} ${type}: ERROR: ${ 8 | typeof err === 'object' ? JSON.stringify(err) : err 9 | }`, 10 | message: { 11 | err: `Error occurred in apiRouter.${method}. Check server logs for more details.`, 12 | }, 13 | }; 14 | }, 15 | }; 16 | 17 | module.exports = utils; 18 | -------------------------------------------------------------------------------- /storedemo/README.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | A template repository for a MongoDB - Express - React (w/ Redux) - Node project. 4 | 5 | -------------------------------------------------------------------------------- /storedemo/build/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 2 | 3 | /** 4 | * @license React 5 | * react-dom.production.min.js 6 | * 7 | * Copyright (c) Facebook, Inc. and its affiliates. 8 | * 9 | * This source code is licensed under the MIT license found in the 10 | * LICENSE file in the root directory of this source tree. 11 | */ 12 | 13 | /** 14 | * @license React 15 | * react.production.min.js 16 | * 17 | * Copyright (c) Facebook, Inc. and its affiliates. 18 | * 19 | * This source code is licensed under the MIT license found in the 20 | * LICENSE file in the root directory of this source tree. 21 | */ 22 | 23 | /** 24 | * @license React 25 | * scheduler.production.min.js 26 | * 27 | * Copyright (c) Facebook, Inc. and its affiliates. 28 | * 29 | * This source code is licensed under the MIT license found in the 30 | * LICENSE file in the root directory of this source tree. 31 | */ 32 | 33 | //! Can pass props to Display {database, collection, query}, then just pass those variables as args into useSubscribe(database, collection, query) 34 | -------------------------------------------------------------------------------- /storedemo/build/images/50.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/50.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/Free.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/Free.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/Portrait.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/Portrait.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/beautiful life.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/beautiful life.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/body & arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/body & arms.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/hold me in your arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/hold me in your arms.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/keep it turned on.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/keep it turned on.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/logo.png -------------------------------------------------------------------------------- /storedemo/build/images/my red book.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/my red book.jpeg -------------------------------------------------------------------------------- /storedemo/build/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/transparent.png -------------------------------------------------------------------------------- /storedemo/build/images/whenever you need.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/build/images/whenever you need.jpeg -------------------------------------------------------------------------------- /storedemo/build/index.html: -------------------------------------------------------------------------------- 1 | inventory Demo
-------------------------------------------------------------------------------- /storedemo/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Display from './components/Display.jsx' 3 | 4 | const App = () => { 5 | 6 | return ( 7 | 8 | 13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /storedemo/client/components/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from "react" 2 | import { FaShoppingCart } from "react-icons/fa"; 3 | import { IconContext } from "react-icons"; 4 | 5 | 6 | 7 | const Cart = ({ quantity, handleCartClick }) => { 8 | console.log('quantity', quantity) 9 | console.log(typeof quantity) 10 | if(quantity > 0){ 11 | return( 12 |
13 | 17 |
18 | ) 19 | }else { 20 | return( 21 |
22 | 23 |
24 | ) 25 | } 26 | 27 | 28 | } 29 | 30 | export default Cart; -------------------------------------------------------------------------------- /storedemo/client/components/Display.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo, useRef } from "react" 2 | import InventoryList from './InventoryList.jsx' 3 | import { changeSingleInventoryField } from '../services/inventory' 4 | // import { handleDecrementClickHelper } from '../services/events' 5 | import { useSubscribe } from "../../../libraries/clientlib/customHook"; 6 | import { FaShoppingCart } from "react-icons/fa"; 7 | import Cart from './Cart.jsx' 8 | 9 | 10 | //! Can pass props to Display {database, collection, query}, then just pass those variables as args into useSubscribe(database, collection, query) 11 | const Display = () => { 12 | 13 | const [ inventoryHookOptions, setInventoryHookOptions] = useState({ 14 | database: 'inventoryDemo', 15 | collection: 'inventoryitems', 16 | query: {}, 17 | }) 18 | 19 | const [ inventoryList, endSubscription ] = useSubscribe(inventoryHookOptions); 20 | // const [ cart, setCart ] = useState(0); 21 | const [ shoppingCart, setShoppingCart ] = useState({length: 0}); 22 | const [ pressedCart, setPressedCart ] = useState(false); 23 | const counter = useRef(0); 24 | 25 | 26 | const addToCard = (id) => { 27 | setShoppingCart((oldCart) => { 28 | console.log(counter.current); 29 | const newCart = JSON.parse(JSON.stringify(oldCart)); 30 | (counter.current === 0 || newCart[id] === 'sound') ? newCart[id] = 'sound' : newCart[id] = true 31 | counter.current++; 32 | return newCart; 33 | }) 34 | } 35 | 36 | 37 | 38 | //increment/decrement click function 39 | const handleIncDecClick = (id, field, value) => { 40 | changeSingleInventoryField(id, field, value) 41 | // .then(setCart((prev) => prev + 1)) 42 | .then(addToCard(id)) 43 | } 44 | 45 | const handleCartClick = () => { 46 | setPressedCart(true); 47 | } 48 | 49 | return ( 50 |
51 |
52 |

53 | 54 | Rick Rollin' Records 55 | 56 |

57 |
58 |
59 | 62 |

Record Shop

63 | 69 |
70 |
71 | ); 72 | } 73 | 74 | export default Display; -------------------------------------------------------------------------------- /storedemo/client/components/InventoryList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | import Item from './Item.jsx' 3 | 4 | 5 | const InventoryList = ({ inventoryList, handleIncDecClick, pressedCart, shoppingCart }) => { 6 | 7 | 8 | const inventory = []; 9 | let i = 0; 10 | 11 | // inventory.push( 12 | // 21 | // ) 22 | 23 | //for every item in the database we add an item component for rendering 24 | for(let items in inventoryList){ 25 | const path = 'build/images/' 26 | inventory.push( 27 | 39 | ) 40 | i++; 41 | } 42 | 43 | return ( 44 |
45 | { inventory } 46 |
47 | ); 48 | } 49 | 50 | export default InventoryList; -------------------------------------------------------------------------------- /storedemo/client/components/Item.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | const Item = ( 4 | { item, 5 | description, 6 | quantity, 7 | price, 8 | handleIncDecClick, 9 | id, 10 | imageUrl, 11 | pressedCart, 12 | shoppingCart 13 | }) => { 14 | 15 | if(quantity > 0){ 16 | return( 17 |
18 | {pressedCart === false && 19 | e.currentTarget.src = './images/logo.png'} 22 | /> 23 | } 24 | {pressedCart === true && shoppingCart === false && 25 | e.currentTarget.src = './images/logo.png'} 28 | /> 29 | } 30 | {pressedCart === true && shoppingCart === true && 31 | 37 | } 38 | {pressedCart === true && shoppingCart === 'sound' && 39 | 45 | } 46 | 47 |
48 | {item} 49 |
50 |
51 | {description} 52 |
53 | {/*
54 | {quantity} 55 |
*/} 56 |
57 | ${price} 58 |
59 |
60 | 63 |
64 |
65 | )}else { 66 | return ( <>) 67 | } 68 | } 69 | 70 | export default Item; -------------------------------------------------------------------------------- /storedemo/client/index.css: -------------------------------------------------------------------------------- 1 | /* ul > li { 2 | display: inline-block; 3 | } 4 | 5 | li { 6 | padding-left: 20px; 7 | } */ 8 | 9 | * { 10 | color: #0D0D0D; 11 | padding: 0px; 12 | margin: 0px; 13 | font-family: sans-serif; 14 | } 15 | 16 | h1 { 17 | font-size: 8vw; 18 | text-align: center; 19 | } 20 | 21 | h2 { 22 | font-size: 5vw; 23 | text-align: center; 24 | margin-bottom: 5px; 25 | } 26 | 27 | button { 28 | background-color: transparent; 29 | border: 1px solid rgb(168, 166, 166); 30 | border-radius: 2px; 31 | color: #232323; 32 | margin: 3px; 33 | padding: 3px; 34 | box-shadow: 0 1px rgb(225, 224, 224); 35 | } 36 | 37 | button:hover { 38 | background-color: rgb(247, 244, 244); 39 | } 40 | 41 | button:active { 42 | box-shadow: 0 .2px #666; 43 | transform: translateY(.4px); 44 | } 45 | 46 | .rickPicRight { 47 | width: 5%; 48 | margin-left: 5px; 49 | } 50 | 51 | .cart { 52 | display: flex; 53 | flex-direction: row; 54 | position: relative; 55 | float: right; 56 | top: 10px; 57 | margin-right: 5px; 58 | padding: 0px; 59 | } 60 | 61 | .cartButton { 62 | width: 25px; 63 | display: flex; 64 | } 65 | 66 | .quantity { 67 | position: relative; 68 | font-size: .5em; 69 | margin: 1px; 70 | } 71 | 72 | .rickPicLeft { 73 | width: 5%; 74 | margin-right: 5px; 75 | transform: scaleX(-1); 76 | } 77 | 78 | .header { 79 | margin-top: 10px; 80 | display: flex; 81 | } 82 | 83 | .display { 84 | background-color: #F8F8F8; 85 | border: solid 1px #e0dcdc; 86 | border-radius: 5px; 87 | margin: auto; 88 | width: 90%; 89 | height: 90%; 90 | padding: 5px; 91 | } 92 | 93 | .card { 94 | color: #232323; 95 | display: flex; 96 | flex-direction: column; 97 | background-color:white; 98 | border: 1px solid white; 99 | border-radius: 3px; 100 | margin: 5px; 101 | box-shadow: 5px 5px 5px rgba(26,115,232,0.06); 102 | /* width: 95%; */ 103 | text-align: center; 104 | } 105 | 106 | /* .rickRoll { 107 | position: absolute; 108 | top: 150; 109 | left: 0; 110 | width: 100%; 111 | height: 100%; 112 | } */ 113 | 114 | .cardLayout{ 115 | display: grid; 116 | grid-template-columns: repeat(auto-fit, minmax(250px, 20%)); 117 | margin: auto; 118 | /* flex-wrap: wrap; 119 | justify-content: space-evenly; */ 120 | } 121 | 122 | /* .values{ 123 | color: #232323; 124 | border: solid 1px #808080; 125 | border-radius: 0px; 126 | width: 100%; 127 | text-align: center; 128 | margin-left: -1px; 129 | } 130 | 131 | .label { 132 | display: flex; 133 | justify-content: space-evenly; 134 | margin-bottom: 5px; 135 | font-weight: bold; 136 | } */ 137 | 138 | 139 | 140 | /* 141 | #quantity:hover > .buttonContainer { 142 | display: inline-block; 143 | } 144 | 145 | #quantity { 146 | display: inline-block; 147 | } */ 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /storedemo/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | 9 | root.render ( 10 | 11 | ) -------------------------------------------------------------------------------- /storedemo/client/services/events.js: -------------------------------------------------------------------------------- 1 | import { changeSingleInventoryField, getSingleInventory } from './inventory' 2 | 3 | //helper functions for events 4 | 5 | const handleIncremementClickHelper = async (id, field, value) => { 6 | //wait for result of calling function to increment DB 7 | await changeSingleInventoryField(id, field, value) 8 | //then call the fucntion that gets inventory of single item from DB (update) 9 | // return getSingleInventory(id) 10 | } 11 | 12 | const handleDecrementClickHelper = async (id, field, value) => { 13 | //wait for result of calling function to decrement DB 14 | return changeSingleInventoryField(id, field, value) 15 | //then call the fucntion that gets inventory of single item from DB (update) 16 | // return getSingleInventory(id) 17 | } 18 | 19 | export { 20 | handleIncremementClickHelper, 21 | handleDecrementClickHelper, 22 | } 23 | -------------------------------------------------------------------------------- /storedemo/client/services/inventory.js: -------------------------------------------------------------------------------- 1 | //helper functions for all requests to inventory router 2 | module.exports = inventoryFunctions = { 3 | 4 | getAllInventory: () => { 5 | return fetch('/inventory/all') 6 | .then(data => data.json()) 7 | }, 8 | 9 | getSingleInventory: (id) => { 10 | return fetch('/inventory/getOne/' + id) 11 | .then(data => data.json()) 12 | }, 13 | 14 | createInventory: (item, quantity, description, price) => { 15 | return fetch('/inventory/create', { 16 | method: 'POST', 17 | headers: { 'Content-Type': 'application/json' }, 18 | body: JSON.stringify({ 19 | item: item, 20 | quantity: quantity, 21 | description: description, 22 | price: price, 23 | }) 24 | }); 25 | }, 26 | 27 | changeSingleInventoryField: (id, field, value) => { 28 | return fetch('/inventory/changeSingleField', { 29 | method: 'PATCH', 30 | headers: { 'Content-Type': 'application/json' }, 31 | body: JSON.stringify({ 32 | id: id, 33 | field: field, 34 | value: value, 35 | }) 36 | }) 37 | } 38 | } -------------------------------------------------------------------------------- /storedemo/client/services/ytapi.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/client/services/ytapi.js -------------------------------------------------------------------------------- /storedemo/public/images/50.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/50.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/Free.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/Free.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/Portrait.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/Portrait.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/beautiful life.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/beautiful life.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/body & arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/body & arms.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/hold me in your arms.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/hold me in your arms.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/keep it turned on.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/keep it turned on.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/logo.png -------------------------------------------------------------------------------- /storedemo/public/images/my red book.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/my red book.jpeg -------------------------------------------------------------------------------- /storedemo/public/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/transparent.png -------------------------------------------------------------------------------- /storedemo/public/images/whenever you need.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/LiveStateDB/50e2797239bb8898f14645ab1df6623f12f8084b/storedemo/public/images/whenever you need.jpeg -------------------------------------------------------------------------------- /storedemo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | inventory Demo 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const fs = require('fs'); 4 | 5 | console.log('NODE_ENV: ', process.env.NODE_ENV) 6 | 7 | module.exports = { 8 | entry: path.resolve(__dirname, './storedemo/client/index.js'), 9 | 10 | output: { 11 | path: path.resolve(__dirname, './storedemo/build/'), 12 | filename: 'bundle.js', 13 | publicPath: '/build', 14 | }, 15 | 16 | mode: process.env.NODE_ENV, 17 | 18 | plugins: [ 19 | new HtmlWebpackPlugin({ 20 | title: 'Development', 21 | template: path.resolve(__dirname, './storedemo/public/index.html') 22 | }), 23 | ], 24 | devServer: { 25 | static: { 26 | directory: path.resolve(__dirname, './storedemo/public/'), 27 | }, 28 | // server: { 29 | // type: 'https', 30 | // options: { 31 | // key: fs.readFileSync(path.resolve(__dirname, './server/keys/server.key')), 32 | // cert: fs.readFileSync(path.resolve(__dirname, './server/keys/server.crt')) 33 | // } 34 | // }, 35 | compress: false, 36 | port: 8082, 37 | proxy: { 38 | '/api/**': { 39 | target: 'http://localhost:3000/', 40 | secure: false, 41 | }, 42 | '/inventory/': { 43 | target: 'http://localhost:3000/', 44 | secure: false, 45 | }, 46 | // '/event/**': { 47 | // target: 'https://localhost:3000/', 48 | // secure: true, 49 | // ws: true, 50 | // }, 51 | '/websocket': { 52 | target: 'http://localhost:3000/', 53 | secure: false, 54 | ws: true, 55 | }, 56 | }, 57 | }, 58 | 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.jsx?/, 63 | exclude: /node_modules/, 64 | use: { 65 | loader: 'babel-loader', 66 | options: { 67 | presets: ['@babel/preset-env', '@babel/preset-react'], 68 | }, 69 | }, 70 | }, 71 | { 72 | test: /\.css$/i, 73 | use: [ 74 | // Compiles Sass to CSS 75 | 'style-loader', 76 | 'css-loader', 77 | 'sass-loader', 78 | ], 79 | }, 80 | ], 81 | }, 82 | resolve: { 83 | extensions: ['.js','.jsx','.json'] 84 | } 85 | }; 86 | --------------------------------------------------------------------------------