├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── client-dist ├── css │ ├── app.084652a9.css │ └── chunk-vendors.90d93e1b.css ├── favicon-32x32.png ├── img │ ├── 1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg │ ├── 42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg │ ├── 63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg │ ├── 6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg │ ├── 97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg │ ├── RedisLabsIllustration.bebd0eb3.svg │ ├── e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg │ ├── efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg │ ├── f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg │ ├── f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg │ └── x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg ├── index.html └── js │ ├── app.98a786d7.js │ ├── app.98a786d7.js.map │ ├── chunk-vendors.5dc46f7b.js │ └── chunk-vendors.5dc46f7b.js.map ├── client ├── .env.example ├── .gitignore ├── .prettierrc.js ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon-32x32.png │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── RedisLabsIllustration.svg │ │ └── products │ │ │ ├── 1f1321bb-0542-45d0-9601-2a3d007d5842.jpg │ │ │ ├── 42860491-9f15-43d4-adeb-0db2cc99174a.jpg │ │ │ ├── 63a3c635-4505-4588-8457-ed04fbb76511.jpg │ │ │ ├── 6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg │ │ │ ├── 97a19842-db31-4537-9241-5053d7c96239.jpg │ │ │ ├── e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg │ │ │ ├── efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg │ │ │ ├── f5384efc-eadb-4d7b-a131-36516269c218.jpg │ │ │ ├── f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg │ │ │ └── x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg │ ├── components │ │ ├── Cart.vue │ │ ├── CartItem.vue │ │ ├── CartList.vue │ │ ├── Info.vue │ │ ├── Product.vue │ │ ├── ProductList.vue │ │ └── ResetDataBtn.vue │ ├── config │ │ └── index.js │ ├── main.js │ ├── plugins │ │ ├── axios.js │ │ └── vuetify.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── cart.js │ │ │ └── products.js │ └── styles │ │ └── styles.scss └── vue.config.js ├── docs └── YTThumbnail.png ├── heroku.yml ├── images └── app_preview_image.png ├── marketplace.json ├── preview.png ├── server ├── .env.example ├── .gitignore ├── .prettierrc.js ├── README.md ├── docker-compose.yml ├── package.json ├── pm2.json └── src │ ├── controllers │ ├── Cart │ │ ├── DeleteItemController.js │ │ ├── EmptyController.js │ │ ├── IndexController.js │ │ └── UpdateController.js │ └── Product │ │ ├── IndexController.js │ │ └── ResetController.js │ ├── index.js │ ├── middleware │ └── checkSession.js │ ├── products.json │ ├── routes │ ├── cart.js │ ├── index.js │ └── products.js │ └── services │ └── RedisClient.js └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as vue-build 2 | WORKDIR /app 3 | COPY ./client/package.json ./ 4 | RUN npm install 5 | COPY ./client/ . 6 | RUN npm run build 7 | 8 | FROM node:lts-alpine AS server-build 9 | WORKDIR /app/server 10 | COPY ./server/package.json ./ 11 | RUN npm install 12 | COPY ./server . 13 | COPY --from=vue-build /app/dist ./../client-dist 14 | EXPOSE ${PORT} 15 | CMD ["node", "./src/index.js"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redis Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorial: A Shopping Cart app in NodeJS and Redis JSON 2 | 3 | ## Technical Stack 4 | 5 | - Frontend - Vue.js 6 | - Backend - NodeJS, ExpressJS, Redis(Redis JSON) 7 | 8 | This shopping cart is using Redis and Redis JSON functionalities, allowing you to save JSON as keys using methods like json_get and json_set. 9 | 10 | 11 | ## How it works 12 | 13 | ### How the data is stored: 14 | 15 | * The products data is stored in external json file. After first request this data is saved in a JSON data type in Redis like: `JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }'`. 16 | * E.g `JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 2 }'` 17 | * The cart data is stored in a hash like: `HSET cart:{cartId} product:{productId} {productQuantity}`, where cartId is random generated value and stored in user session. 18 | * E.g `HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1` 19 | 20 | ### How the data is modified: 21 | * The product data is modified like `JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }'`. 22 | * E.g `JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }'` 23 | * The cart data is modified like `HSET cart:{cartId} product:{productId} {newProductQuantity}` or `HINCRBY cart:{cartId} product:{productId} {incrementBy}`. 24 | * E.g `HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2` 25 | * E.g `HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1` 26 | * E.g `HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1` 27 | * Product can be removed from cart like `HDEL cart:{cartId} product:{productId}` 28 | * E.g `HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6` 29 | * Cart can be cleared using `HGETALL cart:{cartId}` and then `HDEL cart:{cartId} {productKey}` in loop. 30 | * E.g `HGETALL cart:77f7fc881edc2f558e683a230eac217d` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842` => `HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842` 31 | * All carts can be deleted when reset data is requested like: `SCAN {cursor} MATCH cart:*` and then `DEL cart:{cartId}` in loop. 32 | * E.g `SCAN {cursor} MATCH cart:*` => `cart:77f7fc881edc2f558e683a230eac217d`, `cart:217dedc2f558e683a230eac77f7fc881`, `cart:1ede77f558683a230eac7fc88217dc2f` => `DEL cart:77f7fc881edc2f558e683a230eac217d`, `DEL cart:217dedc2f558e683a230eac77f7fc881`, `DEL cart:1ede77f558683a230eac7fc88217dc2f` 33 | 34 | ### How the data is accessed: 35 | * Products: `SCAN {cursor} MATCH product:*` to get all product keys and then `JSON.GET {productKey}` in loop. 36 | * E.g `SCAN {cursor} MATCH product:*` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842` => `JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1`, `JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842` 37 | * Cart: `HGETALL cart:{cartId}`to get quantity of products and `JSON.GET product:{productId}` to get products data in loop. 38 | * E.g `HGETALL cart:77f7fc881edc2f558e683a230eac217d` => `product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1)`, `product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0)`, `product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2)` => `JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6`, `JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12`, `JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842` 39 | * HGETALL returns array of keys and corresponding values from hash data type. 40 | 41 | ## Hot to run it locally? 42 | 43 | ### Prerequisites 44 | 45 | - Node - v12.19.0 46 | - NPM - v6.14.8 47 | - Docker - v19.03.13 (optional) 48 | 49 | ### Local installation 50 | 51 | Go to server folder (`cd ./server`) and then: 52 | 53 | ``` 54 | # Environmental variables 55 | 56 | Copy `.env.example` to `.env` file and fill environmental variables 57 | 58 | REDIS_PORT: Redis port (default: 6379) 59 | REDIS_HOST: Redis host (default: 127.0.0.1) 60 | REDIS_PASSWORD: Redis password (default: demo) 61 | 62 | cp .env.example .env 63 | 64 | # Run docker compose or install redis with RedisJson module manually. You can also go to https://redislabs.com/try-free/ and obtain necessary environmental variables 65 | 66 | docker network create global 67 | docker-compose up -d --build 68 | 69 | # Install dependencies 70 | 71 | npm install 72 | 73 | # Run dev server 74 | 75 | npm run dev 76 | ``` 77 | 78 | Go to client folder (`cd ./client`) and then: 79 | 80 | ``` 81 | # Environmental variables 82 | 83 | Copy `.env.example` to `.env` file 84 | 85 | cp .env.example .env 86 | 87 | # Install dependencies 88 | 89 | npm install 90 | 91 | # Serve locally 92 | 93 | npm run serve 94 | ``` 95 | 96 | ## Deployment 97 | 98 | To make deploys work, you need to create free account in https://redislabs.com/try-free/, create Redis instance with `RedisJson` module and get informations - REDIS_ENDPOINT_URI and REDIS_PASSWORD. You must pass them as environmental variables. 99 | 100 | ### Google Cloud Run 101 | 102 | [![Run on Google 103 | Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs.git) 104 | 105 | ### Heroku 106 | 107 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 108 | 109 | ### Vercel 110 | 111 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs&env=REDIS_ENDPOINT_URI,REDIS_PASSWORD) 112 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Redis Shopping Cart", 3 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.", 4 | "repository": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs", 5 | "keywords": [ 6 | "NodeJS", 7 | "ExpressJS", 8 | "Redis", 9 | "Shopping Cart", 10 | "json_get", 11 | "json_set" 12 | ], 13 | "env": { 14 | "REDIS_ENDPOINT_URI": { 15 | "description": "Redis server URI", 16 | "required": true 17 | }, 18 | "REDIS_PASSWORD": { 19 | "description": "Redis password", 20 | "required": true 21 | } 22 | }, 23 | "stack": "container" 24 | } 25 | -------------------------------------------------------------------------------- /client-dist/css/app.084652a9.css: -------------------------------------------------------------------------------- 1 | body{background-image:url(../img/RedisLabsIllustration.bebd0eb3.svg);background-color:#f8f8fb;background-repeat:no-repeat;background-size:340px;background-position:100% 0}#app{background:none}.text{word-break:normal}input{text-align:center}.v-input__slot,input{margin:0!important}.v-input__slot:before,.v-input__slot:before:active{border-style:none!important;content:none}.h-full{height:100%} -------------------------------------------------------------------------------- /client-dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/favicon-32x32.png -------------------------------------------------------------------------------- /client-dist/img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg -------------------------------------------------------------------------------- /client-dist/img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg -------------------------------------------------------------------------------- /client-dist/img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg -------------------------------------------------------------------------------- /client-dist/img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg -------------------------------------------------------------------------------- /client-dist/img/97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/97a19842-db31-4537-9241-5053d7c96239.3356499c.jpg -------------------------------------------------------------------------------- /client-dist/img/RedisLabsIllustration.bebd0eb3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | -------------------------------------------------------------------------------- /client-dist/img/e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/e182115a-63d2-42ce-8fe0-5f696ecdfba6.0516fcb6.jpg -------------------------------------------------------------------------------- /client-dist/img/efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/efe0c7a3-9835-4dfb-87e1-575b7d06701a.e2ac5748.jpg -------------------------------------------------------------------------------- /client-dist/img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg -------------------------------------------------------------------------------- /client-dist/img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg -------------------------------------------------------------------------------- /client-dist/img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client-dist/img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg -------------------------------------------------------------------------------- /client-dist/index.html: -------------------------------------------------------------------------------- 1 | Redis Shopping Cart
-------------------------------------------------------------------------------- /client-dist/js/app.98a786d7.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var r,s,o=e[0],i=e[1],d=e[2],l=0,f=[];l0?1:t.name.localeCompare(e.name)<0?-1:0})),a("setProducts",n),e.abrupt("return",n);case 8:case"end":return e.stop()}}),e)})))()},reset:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.dispatch,e.next=3,u.post("/api/products/reset");case 3:return e.next=5,a("fetch");case 5:return e.next=7,a("cart/fetch",null,{root:!0});case 7:case"end":return e.stop()}}),e)})))()}},b={state:f,getters:p,mutations:v,actions:m,namespaced:!0},_={items:[]},h=function(){return _},g={getItems:function(t){return t.items}},x={setItems:function(t,e){t.items=e}},C={fetch:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a,r,c;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.commit,e.next=3,u.get("/api/cart");case 3:return r=e.sent,c=r.data,a("setItems",c),e.abrupt("return",c);case 7:case"end":return e.stop()}}),e)})))()},save:function(t,e){return Object(n["a"])(regeneratorRuntime.mark((function a(){var r,c,n,s,o,i;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return r=t.dispatch,c=e.id,n=e.quantity,s=e.incrementBy,a.next=4,u.put("/api/cart/".concat(c),{quantity:n,incrementBy:s});case 4:return o=a.sent,i=o.data,a.next=8,r("fetch");case 8:return a.next=10,r("products/fetch",null,{root:!0});case 10:return a.abrupt("return",i);case 11:case"end":return a.stop()}}),a)})))()},delete:function(t,e){return Object(n["a"])(regeneratorRuntime.mark((function a(){var r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return r=t.dispatch,a.next=3,u.delete("/api/cart/".concat(e));case 3:return a.next=5,r("fetch");case 5:return a.next=7,r("products/fetch",null,{root:!0});case 7:case"end":return a.stop()}}),a)})))()},empty:function(t){return Object(n["a"])(regeneratorRuntime.mark((function e(){var a;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return a=t.dispatch,e.next=3,u.delete("/api/cart");case 3:return e.next=5,a("fetch");case 5:return e.next=7,a("products/fetch",null,{root:!0});case 7:case"end":return e.stop()}}),e)})))()}},w={state:h,getters:g,mutations:x,actions:C,namespaced:!0};r["a"].use(c["a"]);var y=new c["a"].Store({state:{},getters:{},mutations:{},actions:{},modules:{products:b,cart:w}}),j=a("f309");r["a"].use(j["a"]);var O=new j["a"]({}),k=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-app",[a("v-container",[a("div",{staticClass:"my-8 d-flex align-center"},[a("div",{staticClass:"pa-4 rounded-lg red darken-1"},[a("v-icon",{attrs:{color:"white",size:"45"}},[t._v("mdi-cart-plus")])],1),a("h1",{staticClass:"ml-6 font-weight-regular"},[t._v("Shopping Cart demo")])])]),a("v-container",[a("v-row",[a("v-col",{attrs:{cols:"12",sm:"7",md:"8"}},[a("info"),a("product-list",{attrs:{products:t.products}})],1),a("v-col",{staticClass:"d-flex flex-column",attrs:{cols:"12",sm:"5",md:"4"}},[a("cart"),a("reset-data-btn",{staticClass:"mt-6"})],1)],1),a("v-footer",{staticClass:"mt-12 pa-0"},[t._v(" © Copyright 2021 | All Rights Reserved to Redis Labs ")])],1)],1)},I=[],E=a("5530"),V=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-card",{staticClass:"mr-md-4",attrs:{elevation:"5",dark:""}},[a("v-card-title",{staticClass:"pa-3"},[a("v-icon",{staticClass:"mr-2"},[t._v("mdi-cart")]),t._v(" Shopping cart ")],1),t.items.length?a("v-card-text",{staticClass:"pa-3"},[a("cart-list",{attrs:{items:t.items}}),a("v-divider",{staticClass:"mt-6 mb-2"}),a("div",{staticClass:"text-right text title"},[t._v(" Total: "),a("span",{staticClass:"font-weight-black"},[t._v("$"+t._s(t.total))])])],1):a("v-card-text",{staticClass:"pa-3 text-center"},[a("v-icon",{attrs:{"x-large":""}},[t._v("mdi-cart")]),a("p",[t._v(" Cart is Empty. Please add items. ")])],1),a("v-card-actions",{staticClass:"pa-3 justify-space-between"},[a("v-btn",{attrs:{outlined:"",color:"orange"},on:{click:t.emptyCart}},[a("span",{staticClass:"d-xs-flex d-none d-xl-flex"},[t._v("Clear cart")]),a("v-icon",{attrs:{right:"",dark:""}},[t._v(" mdi-close-circle-outline ")])],1),a("v-btn",{staticClass:"primary"},[t._v(" Checkout "),a("v-icon",{attrs:{right:"",dark:""}},[t._v(" mdi-check ")])],1)],1)],1)},T=[],S=(a("d81d"),a("13d5"),function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-row",t._l(t.items,(function(e){return a("v-col",{key:e.id,attrs:{cols:"12"}},[a("cart-item",{attrs:{item:e},on:{save:t.save,delete:t.remove}})],1)})),1)}),R=[],N=(a("7db0"),function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("v-card",{staticClass:"secondary rounded-lg px-2 pr-lg-2 pl-lg-0 mb-2"},[r("v-row",[r("v-col",{staticClass:"py-0 d-lg-flex d-sm-none d-md-none pl-0 pl-sm-3 pl-md-3 pl-lg-3",attrs:{cols:"4",lg:"3",md:"0"}},[r("v-img",{staticClass:"rounded-lg d-lg-flex d-md-none",attrs:{"min-height":"100%",src:a("2204")("./"+t.item.id+".jpg")}})],1),r("v-col",{attrs:{cols:"8",lg:"9",md:"12",sm:"12"}},[r("v-card-title",{staticClass:"text-subtitle-1 text-xl-h6 pa-0"},[t._v(" "+t._s(t.item.name)+" ")]),r("v-card-actions",{staticClass:"justify-space-between text-xl-h6 px-0"},[t._v(" $"+t._s(t.item.priceSum)+" "),r("v-btn-toggle",{staticClass:"secondary",attrs:{multiple:"",rounded:""}},[r("v-btn",{attrs:{small:""},on:{click:function(e){return t.incrementItem(-1)}}},[t._v(" - ")]),r("v-btn",{attrs:{small:""}},[r("v-text-field",{staticStyle:{"max-width":"10px"},on:{input:t.onItemQuantityChange},model:{value:t.itemQuantity,callback:function(e){t.itemQuantity=e},expression:"itemQuantity"}})],1),r("v-btn",{attrs:{disabled:!t.item.stock,small:""},on:{click:function(e){return t.incrementItem(1)}}},[t._v(" + ")])],1)],1)],1)],1)],1)}),q=[],H={name:"CartItem",props:{item:{type:Object,required:!0}},data:function(){return{itemQuantity:0}},watch:{item:{immediate:!0,handler:function(t){this.itemQuantity=parseInt(t.quantity)}}},methods:{onItemQuantityChange:function(){this.$emit("save",{id:this.item.id,quantity:this.itemQuantity})},deleteItem:function(t){this.$emit("delete",t)},incrementItem:function(t){this.itemQuantity+t!==0?this.$emit("save",{id:this.item.id,incrementBy:t}):this.deleteItem(this.item.id)}}},L=H,A=a("2877"),P=a("6544"),D=a.n(P),B=a("8336"),J=a("a609"),$=a("b0af"),G=a("99d9"),Q=a("62ad"),M=a("adda"),U=a("0fd9"),F=a("8654"),Y=Object(A["a"])(L,N,q,!1,null,null,null),K=Y.exports;D()(Y,{VBtn:B["a"],VBtnToggle:J["a"],VCard:$["a"],VCardActions:G["a"],VCardTitle:G["d"],VCol:Q["a"],VImg:M["a"],VRow:U["a"],VTextField:F["a"]});var W={name:"CartList",components:{CartItem:K},props:{items:{type:Array,required:!1,defaultValue:function(){return[]}}},computed:Object(E["a"])({},Object(c["c"])({products:"products/getProducts"})),methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({saveItems:"cart/save",deleteItem:"cart/delete"})),{},{save:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){var r,c,n;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:if(a.prev=0,""!==t.quantity){a.next=3;break}return a.abrupt("return");case 3:if(!t.quantity){a.next=14;break}if(t.quantity=parseInt(t.quantity),0!==t.quantity){a.next=9;break}return a.next=8,e.remove(t.id);case 8:return a.abrupt("return");case 9:if(r=e.items.find((function(e){return e.id===t.id})),c=e.products.find((function(e){return e.id===t.id})),n=parseInt(r.quantity),!(t.quantity>n&&t.quantity>n+c.stock)){a.next=14;break}return a.abrupt("return");case 14:return a.next=16,e.saveItems(t);case 16:a.next=21;break;case 18:a.prev=18,a.t0=a["catch"](0),console.error(a.t0);case 21:case"end":return a.stop()}}),a,null,[[0,18]])})))()},remove:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,e.deleteItem(t);case 3:a.next=8;break;case 5:a.prev=5,a.t0=a["catch"](0),console.error(a.t0);case 8:case"end":return a.stop()}}),a,null,[[0,5]])})))()}})},z=W,X=Object(A["a"])(z,S,R,!1,null,null,null),Z=X.exports;D()(X,{VCol:Q["a"],VRow:U["a"]});var tt={name:"Cart",components:{CartList:Z},computed:Object(E["a"])(Object(E["a"])({},Object(c["c"])({cartItems:"cart/getItems"})),{},{items:function(){return this.cartItems?this.cartItems.map((function(t){var e=t.quantity,a=t.product;return Object(E["a"])(Object(E["a"])({},a),{},{quantity:e,priceSum:e*a.price})})):[]},total:function(){return this.cartItems?this.cartItems.reduce((function(t,e){var a=e.quantity,r=e.product.price;return t+a*r}),0):0}}),created:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.fetchCart();case 2:case"end":return e.stop()}}),e)})))()},methods:Object(E["a"])({},Object(c["b"])({fetchCart:"cart/fetch",emptyCart:"cart/empty"}))},et=tt,at=a("ce7e"),rt=a("132d"),ct=Object(A["a"])(et,V,T,!1,null,null,null),nt=ct.exports;D()(ct,{VBtn:B["a"],VCard:$["a"],VCardActions:G["a"],VCardText:G["c"],VCardTitle:G["d"],VDivider:at["a"],VIcon:rt["a"]});var st=function(){var t=this,e=t.$createElement,a=t._self._c||e;return t.products.length?a("v-row",{attrs:{align:"stretch"}},t._l(t.products,(function(e){return a("product",{key:e.id,attrs:{product:e},on:{add:t.addToCart}})})),1):a("v-row",[a("p",[t._v(" No products in store ")])])},ot=[],it=function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("v-col",{attrs:{cols:"6",sm:"6",md:"6",lg:"4"}},[r("v-card",{staticClass:"h-full",attrs:{disabled:0===t.product.stock}},[r("div",{staticClass:"d-flex justify-center"},[r("v-img",{attrs:{"max-width":"65%",src:a("2204")("./"+t.product.id+".jpg")}})],1),r("v-card-title",{staticClass:"pa-3 text-subtitle-1 text-xl-h6"},[t._v(" "+t._s(t.product.name)+" ")]),r("v-card-subtitle",{staticClass:"pa-3 text-subtitle-1 text-xl-h6"},[t._v(" $"+t._s(t.product.price)+" ")]),r("v-card-text",{staticClass:"pa-3 text-left text caption"}),r("v-divider"),r("v-card-actions",{staticClass:"pa-3 justify-space-between"},[r("span",[t._v(t._s(t.product.stock?t.product.stock+" in":"out of")+" stock")]),r("v-btn",{staticClass:"success",attrs:{disabled:0===t.product.stock},on:{click:function(e){return t.$emit("add",t.product.id)}}},[r("span",{staticClass:"d-xs-flex d-none d-xl-flex"},[t._v("Add to cart")]),r("v-icon",{attrs:{right:"",dark:""}},[t._v("mdi-cart-plus")])],1)],1)],1)],1)},dt=[],ut={name:"Product",props:{product:{type:Object,required:!0}}},lt=ut,ft=Object(A["a"])(lt,it,dt,!1,null,null,null),pt=ft.exports;D()(ft,{VBtn:B["a"],VCard:$["a"],VCardActions:G["a"],VCardSubtitle:G["b"],VCardText:G["c"],VCardTitle:G["d"],VCol:Q["a"],VDivider:at["a"],VIcon:rt["a"],VImg:M["a"]});var vt={name:"ProductList",props:{products:{type:Array,required:!1,defaultValue:function(){return[]}}},components:{Product:pt},methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({saveItem:"cart/save"})),{},{addToCart:function(t){var e=this;return Object(n["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,e.saveItem({id:t,incrementBy:1});case 3:a.next=8;break;case 5:a.prev=5,a.t0=a["catch"](0),console.error(a.t0);case 8:case"end":return a.stop()}}),a,null,[[0,5]])})))()}})},mt=vt,bt=Object(A["a"])(mt,st,ot,!1,null,null,null),_t=bt.exports;D()(bt,{VRow:U["a"]});var ht=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-btn",{staticClass:"mx-auto error",on:{click:t.resetData}},[t._v(" Reset data "),a("v-icon",{attrs:{right:"",dark:""}},[t._v("mdi-restore")])],1)},gt=[],xt={computed:Object(E["a"])({},Object(c["c"])({cartItems:"cart/getItems"})),methods:Object(E["a"])(Object(E["a"])({},Object(c["b"])({reset:"products/reset"})),{},{resetData:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.reset();case 3:e.next=8;break;case 5:e.prev=5,e.t0=e["catch"](0),console.error(e.t0);case 8:case"end":return e.stop()}}),e,null,[[0,5]])})))()}})},Ct=xt,wt=Object(A["a"])(Ct,ht,gt,!1,null,null,null),yt=wt.exports;D()(wt,{VBtn:B["a"],VIcon:rt["a"]});var jt=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("v-alert",{staticClass:"mb-6",attrs:{text:"",dense:"",color:"info",border:"left"}},[a("v-row",{attrs:{align:"center","no-gutters":""},on:{click:function(e){t.alert=!t.alert}}},[a("v-col",{staticClass:"grow"},[a("h3",{staticClass:"headline"},[t._v(" How it works? ")])]),a("v-col",{staticClass:"shrink"},[a("v-btn",{attrs:{color:"info",outlined:""}},[t._v(" "+t._s(t.alert?"Collapse":"View more")+" ")])],1)],1),a("v-divider",{directives:[{name:"show",rawName:"v-show",value:t.alert,expression:"alert"}],staticClass:"my-4 info",staticStyle:{opacity:"0.22"}}),a("div",{directives:[{name:"show",rawName:"v-show",value:t.alert,expression:"alert"}]},[a("ol",[a("li",[t._v("How the data is stored:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("The products data is stored in external json file. After first request this data is saved in a JSON data type in Redis like: "),a("code",[t._v('JSON.SET product:{productId} . JSON.SET product:{productId} . \'{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }\'')]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v('JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . \'{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 2 }\'')])])]),a("li",[t._v("The cart data is stored in a hash like: "),a("code",[t._v("HSET cart:{cartId} product:{productId} {productQuantity}")]),t._v(", where cartId is random generated value and stored in user session.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1")])])])]),a("li",[t._v("How the data is modified:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("The product data is modified like "),a("code",[t._v('JSON.SET product:{productId} . \'{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }\'')]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v('JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . \'{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }')])])]),a("li",[t._v("The cart data is modified like "),a("code",[t._v("HSET cart:{cartId} product:{productId} {newProductQuantity}")]),t._v(" or "),a("code",[t._v("HINCRBY cart:{cartId} product:{productId} {incrementBy}")]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2")])]),a("li",[t._v("E.g "),a("code",[t._v("HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1")])]),a("li",[t._v("E.g "),a("code",[t._v("HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1")])])]),a("li",[t._v("Product can be removed from cart like "),a("code",[t._v("HDEL cart:{cartId} product:{productId}")]),t._v(".")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")])])]),a("li",[t._v("Cart can be cleared using "),a("code",[t._v("HGETALL cart:{cartId}")]),t._v(" and then "),a("code",[t._v("HDEL cart:{cartId} {productKey}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HGETALL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842")]),t._v(" => "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])]),a("li",[t._v("All carts can be deleted when reset data is requested like: "),a("code",[t._v("SCAN {cursor} MATCH cart:*")]),t._v(" and then "),a("code",[t._v("DEL cart:{cartId}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("SCAN {cursor} MATCH cart:*")]),t._v(" => "),a("code",[t._v("cart:77f7fc881edc2f558e683a230eac217d")]),t._v(", "),a("code",[t._v("cart:217dedc2f558e683a230eac77f7fc881")]),t._v(", "),a("code",[t._v("cart:1ede77f558683a230eac7fc88217dc2f")]),t._v(" => "),a("code",[t._v("DEL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(", "),a("code",[t._v("DEL cart:217dedc2f558e683a230eac77f7fc881")]),t._v(", "),a("code",[t._v("DEL cart:1ede77f558683a230eac7fc88217dc2f")])])])]),a("li",[t._v("How the data is accessed:")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("Products: "),a("code",[t._v("SCAN {cursor} MATCH product:*")]),t._v(" to get all product keys and then "),a("code",[t._v("JSON.GET {productKey}")]),t._v(" in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("SCAN {cursor} MATCH product:*")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842")]),t._v(" => "),a("code",[t._v("JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1")]),t._v(", "),a("code",[t._v("JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])]),a("li",[t._v("Cart: "),a("code",[t._v("HGETALL cart:{cartId}")]),t._v(" to get quantity of products and "),a("code",[t._v("JSON.GET product:{productId}")]),t._v(" to get products data in loop.")]),a("ul",{staticClass:"mb-5"},[a("li",[t._v("E.g "),a("code",[t._v("HGETALL cart:77f7fc881edc2f558e683a230eac217d")]),t._v(" => "),a("code",[t._v("product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1)")]),t._v(", "),a("code",[t._v("product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0)")]),t._v(", "),a("code",[t._v("product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2)")]),t._v(" => "),a("code",[t._v("JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6")]),t._v(", "),a("code",[t._v("JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12")]),t._v(", "),a("code",[t._v("JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842")])])])])])])],1)},Ot=[],kt={data:function(){return{alert:!1}}},It=kt,Et=a("0798"),Vt=Object(A["a"])(It,jt,Ot,!1,null,null,null),Tt=Vt.exports;D()(Vt,{VAlert:Et["a"],VBtn:B["a"],VCol:Q["a"],VDivider:at["a"],VRow:U["a"]});var St={name:"App",components:{ProductList:_t,Cart:nt,ResetDataBtn:yt,Info:Tt},computed:Object(E["a"])({},Object(c["c"])({products:"products/getProducts"})),created:function(){var t=this;return Object(n["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.fetchProducts();case 2:case"end":return e.stop()}}),e)})))()},methods:Object(E["a"])({},Object(c["b"])({fetchProducts:"products/fetch"}))},Rt=St,Nt=a("7496"),qt=a("a523"),Ht=a("553a"),Lt=Object(A["a"])(Rt,k,I,!1,null,null,null),At=Lt.exports;D()(Lt,{VApp:Nt["a"],VCol:Q["a"],VContainer:qt["a"],VFooter:Ht["a"],VIcon:rt["a"],VRow:U["a"]});a("3c61");r["a"].config.productionTip=!1,new r["a"]({vuetify:O,store:y,render:function(t){return t(At)}}).$mount("#app")},"6fde":function(t,e,a){t.exports=a.p+"img/f9a6d214-1c38-47ab-a61c-c99a59438b12.fb6ae991.jpg"},"74a0":function(t,e,a){t.exports=a.p+"img/x341115a-63d2-42ce-8fe0-5f696ecdfca6.995189cf.jpg"},7862:function(t,e,a){t.exports=a.p+"img/f5384efc-eadb-4d7b-a131-36516269c218.96b7670b.jpg"},"79bb":function(t,e,a){t.exports=a.p+"img/63a3c635-4505-4588-8457-ed04fbb76511.ca867c58.jpg"},eb8f:function(t,e,a){t.exports=a.p+"img/1f1321bb-0542-45d0-9601-2a3d007d5842.64837cbc.jpg"},ec1e:function(t,e,a){t.exports=a.p+"img/42860491-9f15-43d4-adeb-0db2cc99174a.1e6a24b0.jpg"},f005:function(t,e,a){t.exports=a.p+"img/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.731dc060.jpg"},f121:function(t,e,a){t.exports={apiUrl:"http://localhost:3000"}}}); 2 | //# sourceMappingURL=app.98a786d7.js.map -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=http://localhost:3000 2 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | node_modules 3 | package-lock.json 4 | /dist 5 | .env 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'none', 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | proseWrap: 'always', 11 | htmlWhitespaceSensitivity: 'strict', 12 | endOfLine: 'lf', 13 | arrowParens: 'avoid' 14 | }; 15 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Redis shopping cart UI 2 | 3 | ## Development 4 | 5 | ``` 6 | # Environmental variables 7 | 8 | Copy `.env.example` to `.env` 9 | 10 | cp .env.example .env 11 | 12 | # Install dependencies 13 | 14 | npm install 15 | 16 | # Serve locally 17 | 18 | npm run serve 19 | ``` 20 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-shopping-cart-client", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.", 6 | "keywords": [ 7 | "VueJS", 8 | "Shopping Cart" 9 | ], 10 | "author": "RemoteCraftsmen.com", 11 | "scripts": { 12 | "serve": "vue-cli-service serve", 13 | "build": "vue-cli-service build", 14 | "lint": "vue-cli-service lint", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "dependencies": { 18 | "axios": "^0.21.1", 19 | "core-js": "^3.6.5", 20 | "vue": "^2.6.12", 21 | "vuelidate": "^0.7.6", 22 | "vuetify": "^2.4.0", 23 | "vuex": "^3.6.0" 24 | }, 25 | "devDependencies": { 26 | "@vue/cli-plugin-babel": "~4.5.0", 27 | "@vue/cli-plugin-eslint": "~4.5.0", 28 | "@vue/cli-service": "~4.5.0", 29 | "babel-eslint": "^10.1.0", 30 | "eslint": "^7.17.0", 31 | "eslint-plugin-prettier": "^3.3.1", 32 | "eslint-plugin-vue": "^7.4.1", 33 | "prettier": "^2.2.1", 34 | "sass": "^1.32.0", 35 | "sass-loader": "^10.1.0", 36 | "vue-cli-plugin-vuetify": "~2.0.9", 37 | "vue-template-compiler": "^2.6.11", 38 | "vuetify-loader": "^1.6.0" 39 | }, 40 | "eslintConfig": { 41 | "root": true, 42 | "env": { 43 | "node": true 44 | }, 45 | "extends": [ 46 | "plugin:vue/essential", 47 | "eslint:recommended" 48 | ], 49 | "parserOptions": { 50 | "parser": "babel-eslint" 51 | }, 52 | "rules": {} 53 | }, 54 | "browserslist": [ 55 | "> 1%", 56 | "last 2 versions", 57 | "not dead" 58 | ], 59 | "_id": "redis-shopping-cart-client@1.0.0", 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /client/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Redis Shopping Cart 9 | 13 | 17 | 18 | 19 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 65 | -------------------------------------------------------------------------------- /client/src/assets/RedisLabsIllustration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | -------------------------------------------------------------------------------- /client/src/assets/products/1f1321bb-0542-45d0-9601-2a3d007d5842.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/1f1321bb-0542-45d0-9601-2a3d007d5842.jpg -------------------------------------------------------------------------------- /client/src/assets/products/42860491-9f15-43d4-adeb-0db2cc99174a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/42860491-9f15-43d4-adeb-0db2cc99174a.jpg -------------------------------------------------------------------------------- /client/src/assets/products/63a3c635-4505-4588-8457-ed04fbb76511.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/63a3c635-4505-4588-8457-ed04fbb76511.jpg -------------------------------------------------------------------------------- /client/src/assets/products/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg -------------------------------------------------------------------------------- /client/src/assets/products/97a19842-db31-4537-9241-5053d7c96239.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/97a19842-db31-4537-9241-5053d7c96239.jpg -------------------------------------------------------------------------------- /client/src/assets/products/e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg -------------------------------------------------------------------------------- /client/src/assets/products/efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg -------------------------------------------------------------------------------- /client/src/assets/products/f5384efc-eadb-4d7b-a131-36516269c218.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/f5384efc-eadb-4d7b-a131-36516269c218.jpg -------------------------------------------------------------------------------- /client/src/assets/products/f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg -------------------------------------------------------------------------------- /client/src/assets/products/x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/client/src/assets/products/x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg -------------------------------------------------------------------------------- /client/src/components/Cart.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 87 | -------------------------------------------------------------------------------- /client/src/components/CartItem.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 104 | -------------------------------------------------------------------------------- /client/src/components/CartList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 87 | -------------------------------------------------------------------------------- /client/src/components/Info.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 98 | -------------------------------------------------------------------------------- /client/src/components/Product.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /client/src/components/ProductList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | -------------------------------------------------------------------------------- /client/src/components/ResetDataBtn.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /client/src/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apiUrl: process.env.VUE_APP_API_URL || '' 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import store from './store'; 3 | import vuetify from './plugins/vuetify'; 4 | import App from './App.vue'; 5 | import './styles/styles.scss'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | vuetify, 11 | store, 12 | render: h => h(App) 13 | }).$mount('#app'); 14 | -------------------------------------------------------------------------------- /client/src/plugins/axios.js: -------------------------------------------------------------------------------- 1 | import globalAxios from 'axios'; 2 | import { apiUrl } from '../config'; 3 | 4 | const axios = globalAxios.create({ 5 | baseURL: apiUrl, 6 | withCredentials: true 7 | }); 8 | 9 | export default axios; 10 | -------------------------------------------------------------------------------- /client/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import products from './modules/products'; 5 | import cart from './modules/cart'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | state: {}, 11 | getters: {}, 12 | mutations: {}, 13 | actions: {}, 14 | modules: { 15 | products, 16 | cart 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /client/src/store/modules/cart.js: -------------------------------------------------------------------------------- 1 | import axios from '@/plugins/axios'; 2 | 3 | const initialState = { 4 | items: [] 5 | }; 6 | 7 | const state = () => initialState; 8 | 9 | const getters = { 10 | getItems: state => state.items 11 | }; 12 | 13 | const mutations = { 14 | setItems: (state, items) => { 15 | state.items = items; 16 | } 17 | }; 18 | 19 | const actions = { 20 | async fetch({ commit }) { 21 | const { data } = await axios.get('/api/cart'); 22 | 23 | commit('setItems', data); 24 | 25 | return data; 26 | }, 27 | async save({ dispatch }, { id, quantity, incrementBy }) { 28 | const { data } = await axios.put(`/api/cart/${id}`, { 29 | quantity, 30 | incrementBy 31 | }); 32 | 33 | await dispatch('fetch'); 34 | await dispatch('products/fetch', null, { root: true }); 35 | 36 | return data; 37 | }, 38 | async delete({ dispatch }, id) { 39 | await axios.delete(`/api/cart/${id}`); 40 | 41 | await dispatch('fetch'); 42 | await dispatch('products/fetch', null, { root: true }); 43 | }, 44 | async empty({ dispatch }) { 45 | await axios.delete('/api/cart'); 46 | 47 | await dispatch('fetch'); 48 | await dispatch('products/fetch', null, { root: true }); 49 | } 50 | }; 51 | 52 | export default { 53 | state, 54 | getters, 55 | mutations, 56 | actions, 57 | namespaced: true 58 | }; 59 | -------------------------------------------------------------------------------- /client/src/store/modules/products.js: -------------------------------------------------------------------------------- 1 | import axios from '@/plugins/axios'; 2 | 3 | const initialState = { 4 | products: [] 5 | }; 6 | 7 | const state = () => initialState; 8 | 9 | const getters = { 10 | getProducts: state => state.products 11 | }; 12 | 13 | const mutations = { 14 | setProducts: (state, products) => { 15 | state.products = products; 16 | } 17 | }; 18 | 19 | const actions = { 20 | async fetch({ commit }) { 21 | const { data } = await axios.get('/api/products'); 22 | 23 | const sorted = data.sort((a, b) => { 24 | if (a.name.localeCompare(b.name) > 0) { 25 | return 1; 26 | } 27 | 28 | if (a.name.localeCompare(b.name) < 0) { 29 | return -1; 30 | } 31 | 32 | return 0; 33 | }); 34 | 35 | commit('setProducts', sorted); 36 | 37 | return sorted; 38 | }, 39 | 40 | async reset({ dispatch }) { 41 | await axios.post('/api/products/reset'); 42 | 43 | await dispatch('fetch'); 44 | await dispatch('cart/fetch', null, { root: true }); 45 | } 46 | }; 47 | 48 | export default { 49 | state, 50 | getters, 51 | mutations, 52 | actions, 53 | namespaced: true 54 | }; 55 | -------------------------------------------------------------------------------- /client/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-image: url('../assets/RedisLabsIllustration.svg'); 3 | background-color: #f8f8fb; 4 | background-repeat: no-repeat; 5 | background-size: 340px; 6 | background-position: right top; 7 | } 8 | 9 | #app { 10 | background: none; 11 | } 12 | 13 | .text { 14 | word-break: normal; 15 | } 16 | 17 | input { 18 | text-align: center; 19 | margin: 0 !important; 20 | } 21 | 22 | .v-input__slot { 23 | margin: 0 !important; 24 | } 25 | 26 | .v-input__slot::before { 27 | border-style: none !important; 28 | content: none; 29 | } 30 | 31 | .v-input__slot::before:active { 32 | border-style: none !important; 33 | content: none; 34 | } 35 | 36 | .h-full { 37 | height: 100%; 38 | } 39 | 40 | .v-sheet.v-card:not(.v-sheet--outlined) { 41 | box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 42 | 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%) !important; 43 | } 44 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | transpileDependencies: ['vuetify'], 5 | configureWebpack: { 6 | resolve: { 7 | alias: { 8 | '@': path.resolve(__dirname, 'src/') 9 | } 10 | } 11 | }, 12 | pluginOptions: { 13 | i18n: { 14 | locale: 'en', 15 | fallbackLocale: 'en', 16 | localeDir: 'locales', 17 | enableInSFC: true 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /docs/YTThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/docs/YTThumbnail.png -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/images/app_preview_image.png -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Shopping Cart app in NodeJS", 3 | "description": "Shopping Cart app in NodeJS using Redis JSON feature functionalities", 4 | "type": "Building Block", 5 | "contributed_by": "Redis", 6 | "repo_url": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/master/images/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/basic-redis-shopping-chart-nodejs/archive/main.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "true", 11 | "deploy_buttons": [ 12 | { 13 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs" 14 | }, 15 | { 16 | "vercel": "https://vercel.com/new/git/external?repository-url=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs&env=REDIS_ENDPOINT_URI,REDIS_PASSWORD&envDescription=REDIS_ENDPOINT_URI%20is%20required%20at%20least%20to%20connect%20to%20Redis%20clouding%20server" 17 | }, 18 | { 19 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-shopping-chart-nodejs.git" 20 | } 21 | ], 22 | "language": [ 23 | "JavaScript", 24 | "NodeJS" 25 | ], 26 | "redis_commands": [ 27 | "JSON_GET", 28 | "JSON_SET", 29 | "HGETALL", 30 | "HSET", 31 | "HGET", 32 | "HDEL", 33 | "HINCRBY", 34 | "DEL", 35 | "SCAN" 36 | ], 37 | "redis_use_cases": [], 38 | "redis_features": [ 39 | "JSON" 40 | ], 41 | "app_image_urls": [ 42 | "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/main/preview.png?raw=true" 43 | ], 44 | "youtube_url": "https://www.youtube.com/watch?v=Fvjn5fuUxOU", 45 | "special_tags": [], 46 | "verticals": [ 47 | "Retail" 48 | ], 49 | "markdown": "https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/main/README.md" 50 | } -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-shopping-chart-nodejs/bb9e534a45a5f9cb73dfb15b5bbc604ad0a48b1a/preview.png -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | 3 | # Host and a port. Can be with `redis://` or without. 4 | # Host and a port encoded in redis uri take precedence over other environment variable. 5 | # preferable 6 | REDIS_ENDPOINT_URI=redis://127.0.0.1:6379 7 | 8 | # Or you can set it here (ie. for docker development) 9 | REDIS_HOST=127.0.0.1 10 | REDIS_PORT=6379 11 | 12 | # You can set password here 13 | REDIS_PASSWORD= 14 | 15 | COMPOSE_PROJECT_NAME=redis-shopping-cart 16 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | **/.DS_Store 5 | .idea/ 6 | -------------------------------------------------------------------------------- /server/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'none', 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | proseWrap: 'never', 11 | htmlWhitespaceSensitivity: 'strict', 12 | endOfLine: 'lf', 13 | arrowParens: 'avoid' 14 | }; 15 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Redis shopping cart API 2 | 3 | ## Prerequisites 4 | 5 | - Node - v12.19.0 6 | - NPM - v6.14.8 7 | - Docker - v19.03.13 (optional) 8 | 9 | ## Development 10 | 11 | ``` 12 | # Environmental variables 13 | 14 | Copy `.env.example` to `.env` file and fill environmental variables 15 | 16 | - REDIS_PORT: Redis port (default: 6379) 17 | - REDIS_HOST: Redis host (default: 127.0.0.1) 18 | - REDIS_PASSWORD: Redis password (default: demo) 19 | 20 | cp .env.example .env 21 | 22 | # Run docker compose or install redis with RedisJson module manually. You can also go to https://redislabs.com/try-free/ and obtain necessary environmental variables 23 | 24 | docker network create global 25 | docker-compose up -d --build 26 | 27 | # Install dependencies 28 | 29 | npm install 30 | 31 | # Run dev server 32 | 33 | npm run dev 34 | ``` 35 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redislabs/rejson:latest 6 | container_name: redis.redisshoppingcart.docker 7 | restart: unless-stopped 8 | environment: 9 | REDIS_PASSWORD: ${REDIS_PASSWORD} 10 | command: redis-server --loadmodule "/usr/lib/redis/modules/rejson.so" --requirepass "$REDIS_PASSWORD" 11 | ports: 12 | - 127.0.0.1:${REDIS_PORT}:6379 13 | networks: 14 | - global 15 | networks: 16 | global: 17 | external: true 18 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-shopping-cart-backend", 3 | "version": "1.0.0", 4 | "description": "This shopping cart is using Redis and RedisJson module functionalities, allowing to save JSON as keys using methods like json_get and json_set.", 5 | "keywords": [ 6 | "NodeJS", 7 | "ExpressJS", 8 | "Redis", 9 | "Shopping Cart", 10 | "json_get", 11 | "json_set" 12 | ], 13 | "main": "src/index.js", 14 | "scripts": { 15 | "dev": "nodemon src/index.js", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "RemoteCraftsmen.com", 19 | "license": "MIT", 20 | "dependencies": { 21 | "body-parser": "^1.19.0", 22 | "connect-redis": "^5.0.0", 23 | "cors": "^2.8.5", 24 | "dotenv": "^8.2.0", 25 | "express": "^4.17.1", 26 | "express-session": "^1.17.1", 27 | "http-status-codes": "^2.1.4", 28 | "redis": "^3.0.2", 29 | "redis-rejson": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "RedisShCApp", 5 | "script": "./src/index.js", 6 | "watch": false, 7 | "env_production": { 8 | "NODE_ENV": "production" 9 | }, 10 | "exp_backoff_restart_delay": 250, 11 | "max_restarts": 10, 12 | "min_uptime": 5000, 13 | "max_memory_restart": "150M", 14 | "log_date_format": "YYYY-MM-DD HH:mm" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /server/src/controllers/Cart/DeleteItemController.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class CartDeleteItemController { 4 | constructor(redisClientService) { 5 | this.redisClientService = redisClientService; 6 | } 7 | 8 | async index(req, res) { 9 | const { cartId } = req.session; 10 | const { id: productId } = req.params; 11 | 12 | const quantityInCart = 13 | parseInt(await this.redisClientService.hget(`cart:${cartId}`, `product:${productId}`)) || 0; 14 | 15 | if (quantityInCart) { 16 | await this.redisClientService.hdel(`cart:${cartId}`, `product:${productId}`); 17 | 18 | let productInStore = await this.redisClientService.jsonGet(`product:${productId}`); 19 | 20 | productInStore = JSON.parse(productInStore); 21 | productInStore.stock += quantityInCart; 22 | 23 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore)); 24 | } 25 | 26 | return res.sendStatus(StatusCodes.NO_CONTENT); 27 | } 28 | } 29 | 30 | module.exports = CartDeleteItemController; 31 | -------------------------------------------------------------------------------- /server/src/controllers/Cart/EmptyController.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class CartEmptyController { 4 | constructor(redisClientService) { 5 | this.redisClientService = redisClientService; 6 | } 7 | 8 | async index(req, res) { 9 | const { cartId } = req.session; 10 | 11 | const cartList = await this.redisClientService.hgetall(`cart:${cartId}`); 12 | 13 | if (!cartList) { 14 | return res.sendStatus(StatusCodes.NO_CONTENT); 15 | } 16 | 17 | for (const key of Object.keys(cartList)) { 18 | await this.redisClientService.hdel(`cart:${cartId}`, key); 19 | 20 | let productInStore = await this.redisClientService.jsonGet(key); 21 | 22 | productInStore = JSON.parse(productInStore); 23 | productInStore.stock += parseInt(cartList[key]); 24 | 25 | await this.redisClientService.jsonSet(key, '.', JSON.stringify(productInStore)); 26 | } 27 | 28 | return res.sendStatus(StatusCodes.NO_CONTENT); 29 | } 30 | } 31 | 32 | module.exports = CartEmptyController; 33 | -------------------------------------------------------------------------------- /server/src/controllers/Cart/IndexController.js: -------------------------------------------------------------------------------- 1 | class CartIndexController { 2 | constructor(redisClientService) { 3 | this.redisClientService = redisClientService; 4 | } 5 | 6 | async index(req, res) { 7 | const { cartId } = req.session; 8 | let productList = []; 9 | 10 | const cartList = await this.redisClientService.hgetall(`cart:${cartId}`); 11 | 12 | if (!cartList) { 13 | return res.send(productList); 14 | } 15 | 16 | for (const itemKey of Object.keys(cartList)) { 17 | const product = await this.redisClientService.jsonGet(itemKey); 18 | 19 | productList.push({ product: JSON.parse(product), quantity: cartList[itemKey] }); 20 | } 21 | 22 | return res.send(productList); 23 | } 24 | } 25 | 26 | module.exports = CartIndexController; 27 | -------------------------------------------------------------------------------- /server/src/controllers/Cart/UpdateController.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | 3 | class CartUpdateController { 4 | constructor(redisClientService) { 5 | this.redisClientService = redisClientService; 6 | } 7 | 8 | async index(req, res) { 9 | const { 10 | session: { cartId }, 11 | params: { id: productId } 12 | } = req; 13 | 14 | let { quantity, incrementBy } = req.body; 15 | 16 | let productInStore = await this.redisClientService.jsonGet(`product:${productId}`); 17 | 18 | if (!productInStore) { 19 | return res.status(StatusCodes.NOT_FOUND).send({ message: "Product with this id doesn't exist" }); 20 | } 21 | 22 | let quantityInCart = (await this.redisClientService.hget(`cart:${cartId}`, `product:${productId}`)) || 0; 23 | 24 | quantityInCart = parseInt(quantityInCart); 25 | 26 | productInStore = JSON.parse(productInStore); 27 | const { stock } = productInStore; 28 | 29 | if (quantity) { 30 | quantity = parseInt(quantity); 31 | 32 | if (quantity <= 0) { 33 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Quantity should be greater than 0' }); 34 | } 35 | 36 | const newStock = stock - (quantity - quantityInCart); 37 | 38 | if (newStock < 0) { 39 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Not enough products in stock' }); 40 | } 41 | 42 | await this.redisClientService.hset(`cart:${cartId}`, `product:${productId}`, quantity); 43 | 44 | productInStore.stock = newStock; 45 | 46 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore)); 47 | } 48 | 49 | if (incrementBy) { 50 | incrementBy = parseInt(incrementBy); 51 | 52 | if (incrementBy !== 1 && incrementBy !== -1) { 53 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Value of incrementBy should be 1 or -1' }); 54 | } 55 | 56 | const quantityAfterIncrement = quantityInCart + incrementBy; 57 | 58 | if (quantityAfterIncrement <= 0 || stock - incrementBy < 0) { 59 | return res.status(StatusCodes.BAD_REQUEST).send({ message: "Can't decrement stock to 0" }); 60 | } 61 | 62 | if (stock - incrementBy < 0) { 63 | return res.status(StatusCodes.BAD_REQUEST).send({ message: 'Not enough products in stock' }); 64 | } 65 | 66 | await this.redisClientService.hincrby(`cart:${cartId}`, `product:${productId}`, incrementBy); 67 | 68 | productInStore.stock -= incrementBy; 69 | 70 | await this.redisClientService.jsonSet(`product:${productId}`, '.', JSON.stringify(productInStore)); 71 | } 72 | 73 | return res.sendStatus(StatusCodes.OK); 74 | } 75 | } 76 | 77 | module.exports = CartUpdateController; 78 | -------------------------------------------------------------------------------- /server/src/controllers/Product/IndexController.js: -------------------------------------------------------------------------------- 1 | const { products } = require('../../products.json'); 2 | 3 | class ProductIndexController { 4 | constructor(redisClientService) { 5 | this.redisClientService = redisClientService; 6 | } 7 | 8 | async index(req, res) { 9 | const productKeys = await this.redisClientService.scan('product:*'); 10 | const productList = []; 11 | 12 | if (productKeys.length) { 13 | for (const key of productKeys) { 14 | const product = await this.redisClientService.jsonGet(key); 15 | 16 | productList.push(JSON.parse(product)); 17 | } 18 | 19 | return res.send(productList); 20 | } 21 | 22 | for (const product of products) { 23 | const { id } = product; 24 | 25 | await this.redisClientService.jsonSet(`product:${id}`, '.', JSON.stringify(product)); 26 | 27 | productList.push(product); 28 | } 29 | 30 | return res.send(productList); 31 | } 32 | } 33 | 34 | module.exports = ProductIndexController; 35 | -------------------------------------------------------------------------------- /server/src/controllers/Product/ResetController.js: -------------------------------------------------------------------------------- 1 | const { StatusCodes } = require('http-status-codes'); 2 | const { products } = require('../../products.json'); 3 | 4 | class ProductResetController { 5 | constructor(redisClientService) { 6 | this.redisClientService = redisClientService; 7 | } 8 | 9 | async index(req, res) { 10 | const cartKeys = await this.redisClientService.scan('cart:*'); 11 | 12 | for (const key of cartKeys) { 13 | await this.redisClientService.del(key); 14 | } 15 | 16 | for (const product of products) { 17 | const { id } = product; 18 | 19 | await this.redisClientService.jsonSet(`product:${id}`, '.', JSON.stringify(product)); 20 | } 21 | 22 | return res.sendStatus(StatusCodes.OK); 23 | } 24 | } 25 | 26 | module.exports = ProductResetController; 27 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const redis = require('redis'); 3 | const rejson = require('redis-rejson'); 4 | const session = require('express-session'); 5 | const RedisStore = require('connect-redis')(session); 6 | const path = require('path'); 7 | const bodyParser = require('body-parser'); 8 | const cors = require('cors'); 9 | const RedisClient = require('./services/RedisClient'); 10 | 11 | rejson(redis); 12 | 13 | require('dotenv').config(); 14 | 15 | const { REDIS_ENDPOINT_URI, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, PORT } = process.env; 16 | 17 | const app = express(); 18 | 19 | app.use( 20 | cors({ 21 | origin(origin, callback) { 22 | callback(null, true); 23 | }, 24 | credentials: true 25 | }) 26 | ); 27 | 28 | const redisEndpointUri = REDIS_ENDPOINT_URI 29 | ? REDIS_ENDPOINT_URI.replace(/^(redis\:\/\/)/, '') 30 | : `${REDIS_HOST}:${REDIS_PORT}`; 31 | 32 | const redisClient = redis.createClient(`redis://${redisEndpointUri}`, { 33 | password: REDIS_PASSWORD 34 | }); 35 | 36 | const redisClientService = new RedisClient(redisClient); 37 | 38 | app.set('redisClientService', redisClientService); 39 | 40 | app.use( 41 | session({ 42 | store: new RedisStore({ client: redisClient }), 43 | secret: 'someSecret', 44 | resave: false, 45 | saveUninitialized: false, 46 | rolling: true, 47 | cookie: { 48 | maxAge: 3600 * 1000 * 3 49 | } 50 | }) 51 | ); 52 | 53 | app.use(bodyParser.json()); 54 | 55 | app.use('/', express.static(path.join(__dirname, '../../client-dist'))); 56 | 57 | const router = require('./routes')(app); 58 | 59 | app.use('/api', router); 60 | 61 | const port = PORT || 3000; 62 | 63 | app.listen(port, () => { 64 | console.log(`App listening on port ${port}`); 65 | }); 66 | -------------------------------------------------------------------------------- /server/src/middleware/checkSession.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | module.exports = (req, res, next) => { 4 | if (req.session && req.session.cartId) { 5 | return next(); 6 | } 7 | 8 | req.session.cartId = crypto.randomBytes(16).toString('hex'); 9 | 10 | return next(); 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", 5 | "name": "Brilliant Watch", 6 | "price": "250.00", 7 | "stock": 2 8 | }, 9 | { 10 | "id": "f9a6d214-1c38-47ab-a61c-c99a59438b12", 11 | "name": "Old fashion cellphone", 12 | "price": "24.00", 13 | "stock": 2 14 | }, 15 | { 16 | "id": "1f1321bb-0542-45d0-9601-2a3d007d5842", 17 | "name": "Modern iPhone", 18 | "price": "1000.00", 19 | "stock": 2 20 | }, 21 | { 22 | "id": "f5384efc-eadb-4d7b-a131-36516269c218", 23 | "name": "Beautiful Sunglasses", 24 | "price": "12.00", 25 | "stock": 2 26 | }, 27 | { 28 | "id": "6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345", 29 | "name": "Stylish Cup", 30 | "price": "8.00", 31 | "stock": 2 32 | }, 33 | { 34 | "id": "efe0c7a3-9835-4dfb-87e1-575b7d06701a", 35 | "name": "Herb caps", 36 | "price": "12.00", 37 | "stock": 2 38 | }, 39 | { 40 | "id": "x341115a-63d2-42ce-8fe0-5f696ecdfca6", 41 | "name": "Audiophile Headphones", 42 | "price": "550.00", 43 | "stock": 2 44 | }, 45 | { 46 | "id": "42860491-9f15-43d4-adeb-0db2cc99174a", 47 | "name": "Digital Camera", 48 | "price": "225.00", 49 | "stock": 2 50 | }, 51 | { 52 | "id": "63a3c635-4505-4588-8457-ed04fbb76511", 53 | "name": "Empty Bluray Disc", 54 | "price": "5.00", 55 | "stock": 2 56 | }, 57 | { 58 | "id": "97a19842-db31-4537-9241-5053d7c96239", 59 | "name": "256BG Pendrive", 60 | "price": "60.00", 61 | "stock": 2 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /server/src/routes/cart.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const checkSession = require('../middleware/checkSession'); 4 | const IndexController = require('../controllers/Cart/IndexController'); 5 | const UpdateController = require('../controllers/Cart/UpdateController'); 6 | const DeleteItemController = require('../controllers/Cart/DeleteItemController'); 7 | const EmptyController = require('../controllers/Cart/EmptyController'); 8 | 9 | module.exports = app => { 10 | const redisClientService = app.get('redisClientService'); 11 | 12 | const indexController = new IndexController(redisClientService); 13 | const updateController = new UpdateController(redisClientService); 14 | const deleteItemController = new DeleteItemController(redisClientService); 15 | const emptyController = new EmptyController(redisClientService); 16 | 17 | router.get('/', [checkSession], (...args) => indexController.index(...args)); 18 | router.put('/:id', [checkSession], (...args) => updateController.index(...args)); 19 | router.delete('/:id', [checkSession], (...args) => deleteItemController.index(...args)); 20 | router.delete('/', [checkSession], (...args) => emptyController.index(...args)); 21 | 22 | return router; 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const express = require('express'); 3 | const router = express.Router(); 4 | 5 | module.exports = app => { 6 | fs.readdirSync(__dirname).forEach(function (route) { 7 | route = route.split('.')[0]; 8 | 9 | if (route === 'index') { 10 | return; 11 | } 12 | 13 | router.use(`/${route}`, require(`./${route}`)(app)); 14 | }); 15 | 16 | return router; 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/routes/products.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const checkSession = require('../middleware/checkSession'); 4 | const IndexController = require('../controllers/Product/IndexController'); 5 | const ResetController = require('../controllers/Product/ResetController'); 6 | 7 | module.exports = app => { 8 | const redisClientService = app.get('redisClientService'); 9 | 10 | const indexController = new IndexController(redisClientService); 11 | const resetController = new ResetController(redisClientService); 12 | 13 | router.get('/', [checkSession], (...args) => indexController.index(...args)); 14 | router.post('/reset', (...args) => resetController.index(...args)); 15 | 16 | return router; 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/services/RedisClient.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | 3 | class RedisClient { 4 | constructor(redisClient) { 5 | ['json_get', 'json_set', 'hgetall', 'hset', 'hget', 'hdel', 'hincrby', 'del', 'scan'].forEach( 6 | method => (redisClient[method] = promisify(redisClient[method])) 7 | ); 8 | this.redis = redisClient; 9 | } 10 | 11 | async scan(pattern) { 12 | let matchingKeysCount = 0; 13 | let keys = []; 14 | 15 | const recursiveScan = async (cursor = '0') => { 16 | const [newCursor, matchingKeys] = await this.redis.scan(cursor, 'MATCH', pattern); 17 | cursor = newCursor; 18 | 19 | matchingKeysCount += matchingKeys.length; 20 | keys = keys.concat(matchingKeys); 21 | 22 | if (cursor === '0') { 23 | return keys; 24 | } else { 25 | return await recursiveScan(cursor); 26 | } 27 | }; 28 | 29 | return await recursiveScan(); 30 | } 31 | 32 | jsonGet(key) { 33 | return this.redis.json_get(key); 34 | } 35 | 36 | jsonSet(key, path, json) { 37 | return this.redis.json_set(key, path, json); 38 | } 39 | 40 | hgetall(key) { 41 | return this.redis.hgetall(key); 42 | } 43 | 44 | hset(hash, key, value) { 45 | return this.redis.hset(hash, key, value); 46 | } 47 | 48 | hget(hash, key) { 49 | return this.redis.hget(hash, key); 50 | } 51 | 52 | hdel(hash, key) { 53 | return this.redis.hdel(hash, key); 54 | } 55 | 56 | hincrby(hash, key, incr) { 57 | return this.redis.hincrby(hash, key, incr); 58 | } 59 | 60 | del(key) { 61 | return this.redis.del(key); 62 | } 63 | } 64 | 65 | module.exports = RedisClient; 66 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [{ "src": "server/src/index.js", "use": "@now/node" }], 4 | "routes": [{ "src": "(.*)", "dest": "server/src/index.js" }] 5 | } 6 | --------------------------------------------------------------------------------