├── api ├── items │ ├── sample.dat │ ├── function.json │ └── index.js ├── lists │ ├── sample.dat │ ├── function.json │ └── index.js ├── .funcignore ├── proxies.json ├── .vscode │ └── extensions.json ├── config │ ├── default.json │ ├── custom-environment-variables.json │ └── index.js ├── package.json ├── host.json ├── models │ ├── todoList.js │ ├── todoItem.js │ └── store.js ├── core.js ├── .gitignore └── package-lock.json ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── build │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ └── manifest.json ├── features │ ├── lists │ │ ├── listAPI.js │ │ ├── Lists.js │ │ └── listSlice.js │ ├── items │ │ ├── itemAPI.js │ │ ├── itemSlice.js │ │ └── Items.js │ └── user │ │ └── userSlice.js ├── index.css ├── reportWebVitals.js ├── app │ └── store.js ├── App.js ├── index.js ├── TopBar.js └── Display.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .gitignore ├── package.json ├── .devcontainer ├── devcontainer.json ├── docker-compose.yml └── Dockerfile ├── local-setup.md ├── .github └── workflows │ └── azure-static-web-apps-kind-plant-02300391e.yml └── README.md /api/items/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /api/lists/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/public/logo512.png -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /api/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/favicon.ico -------------------------------------------------------------------------------- /src/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/logo192.png -------------------------------------------------------------------------------- /src/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticwebdev/mongoose-starter/HEAD/src/build/logo512.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /api/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /api/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connectionString": "mongodb://localhost", 4 | "databaseName": "todo" 5 | } 6 | } -------------------------------------------------------------------------------- /api/config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "connectionString": "AZURE_COSMOS_CONNECTION_STRING", 4 | "databaseName": "AZURE_COSMOS_DATABASE_NAME" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "func start", 7 | "test": "echo \"No tests yet...\"" 8 | }, 9 | "dependencies": { 10 | "config": "^3.3.7", 11 | "dotenv": "^16.0.1", 12 | "mongoose": "^5.10.11" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/features/lists/listAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | const baseUrl = '/api/lists'; 3 | 4 | export const listService = { 5 | list: async () => { 6 | const response = await axios.get(baseUrl); 7 | return response.data; 8 | }, 9 | save: async (list) => { 10 | const response = await axios.post(baseUrl, list); 11 | return response.data; 12 | } 13 | } -------------------------------------------------------------------------------- /api/lists/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ], 12 | "route": "lists/{listId?}" 13 | }, 14 | { 15 | "type": "http", 16 | "direction": "out", 17 | "name": "res" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /api/items/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post", 11 | "put", 12 | "delete" 13 | ], 14 | "route": "lists/{listId}/items/{itemId?}" 15 | }, 16 | { 17 | "type": "http", 18 | "direction": "out", 19 | "name": "res" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | // import counterReducer from '../features/counter/counterSlice'; 3 | import itemsReducer from '../features/items/itemSlice'; 4 | import listsReducer from '../features/lists/listSlice'; 5 | import userReducer from '../features/user/userSlice'; 6 | 7 | export const store = configureStore({ 8 | reducer: { 9 | items: itemsReducer, 10 | lists: listsReducer, 11 | user: userReducer 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "api", 3 | "azureFunctions.postDeployTask": "npm install", 4 | "azureFunctions.projectLanguage": "JavaScript", 5 | "azureFunctions.projectRuntime": "~3", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.preDeployTask": "npm prune", 8 | "markdownlint.config": { 9 | "MD028": false, 10 | "MD025": { 11 | "front_matter_title": "" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /api/models/todoList.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const schema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | }, 8 | description: String, 9 | userId: { 10 | type: String, 11 | required: true, 12 | }, 13 | }, { 14 | timestamps: { 15 | createdAt: "createdDate", 16 | updatedAt: "updatedDate" 17 | } 18 | }); 19 | 20 | module.exports.TodoListModel = mongoose.model("TodoList", schema, "TodoList"); -------------------------------------------------------------------------------- /api/core.js: -------------------------------------------------------------------------------- 1 | // Get current user 2 | function getUserId(req) { 3 | // Retrieve client info from request header 4 | const header = req.headers['x-ms-client-principal']; 5 | // The header is encoded in Base64, so we need to convert it 6 | const encoded = Buffer.from(header, 'base64'); 7 | // Convert from Base64 to ascii 8 | const decoded = encoded.toString('ascii'); 9 | // Convert to a JSON object and return the userId 10 | return JSON.parse(decoded).userId; 11 | } 12 | exports.getUserId = getUserId; 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /api/config/index.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | const config = require("config"); 3 | 4 | module.exports.getConfig = async (log) => { 5 | // Load any ENV vars from local .env file 6 | if (process.env.NODE_ENV !== "production") { 7 | dotenv.config(); 8 | } 9 | 10 | // load database configuration 11 | const databaseConfig = config.get("database"); 12 | log("Database config loaded"); 13 | log(databaseConfig); 14 | return { 15 | database: { 16 | connectionString: databaseConfig.connectionString, 17 | databaseName: databaseConfig.databaseName, 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import Display from './Display'; 4 | import { getUserAsync, selectUserDetails } from './features/user/userSlice'; 5 | import TopBar from './TopBar'; 6 | 7 | function App() { 8 | const dispatch = useDispatch(); 9 | 10 | useEffect(() => { 11 | dispatch(getUserAsync()); 12 | }, [dispatch]); 13 | 14 | const userDetails = useSelector(selectUserDetails); 15 | 16 | return ( 17 |
18 | 19 | 20 | {userDetails === '' 21 | ?

Login to see todo items

22 | : 23 | } 24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | import './index.css'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-node-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm install", 10 | "options": { 11 | "cwd": "${workspaceFolder}/api" 12 | } 13 | }, 14 | { 15 | "type": "shell", 16 | "label": "npm install", 17 | "command": "npm install", 18 | "options": { 19 | "cwd": "${workspaceFolder}/api" 20 | } 21 | }, 22 | { 23 | "type": "shell", 24 | "label": "npm prune", 25 | "command": "npm prune --production", 26 | "problemMatcher": [], 27 | "options": { 28 | "cwd": "${workspaceFolder}/api" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /api/models/todoItem.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const schema = new mongoose.Schema({ 4 | id: mongoose.Types.ObjectId, 5 | listId: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | required: true 8 | }, 9 | name: { 10 | type: String, 11 | required: true 12 | }, 13 | userId: { 14 | type: String, 15 | required: true 16 | }, 17 | description: String, 18 | state: { 19 | type: String, 20 | required: true, 21 | default: 'active' 22 | }, 23 | dueDate: Date, 24 | completedDate: Date, 25 | }, { 26 | timestamps: { 27 | createdAt: "createdDate", 28 | updatedAt: "updatedDate" 29 | } 30 | }); 31 | 32 | module.exports.TodoItemModel = mongoose.model("TodoItem", schema, "TodoItem"); -------------------------------------------------------------------------------- /src/TopBar.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | import { selectUserDetails } from "./features/user/userSlice"; 3 | 4 | function TopBar() { 5 | const userDetails = useSelector(selectUserDetails); 6 | 7 | return ( 8 |
9 |
10 | Todo manager 11 | 12 | { 13 | userDetails !== '' // user logged in 14 | ? Welcome, {userDetails} Logout 15 | : Login 16 | } 17 | 18 |
19 |
20 | ) 21 | } 22 | 23 | export default TopBar; -------------------------------------------------------------------------------- /src/features/items/itemAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | const baseUrl = '/api/lists'; 3 | 4 | export const itemService = { 5 | list: async (listId) => { 6 | const response = await axios.get(`${baseUrl}/${listId}/items`); 7 | return response.data; 8 | }, 9 | save: async (item) => { 10 | if (!item.id) { 11 | // New item, post/save 12 | item.state = 'active'; 13 | const response = await axios.post(`${baseUrl}/${item.listId}/items`, item); 14 | return response.data; 15 | } else { 16 | // Existing item updating, put/save 17 | const response = await axios.put(`${baseUrl}/${item.listId}/items/${item.id}`, item); 18 | return response.data; 19 | } 20 | }, 21 | delete: async (item) => { 22 | await axios.delete(`${baseUrl}/${item.listId}/items/${item.id}`); 23 | return item; 24 | } 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@reduxjs/toolkit": "^1.8.1", 4 | "ajv": "^8.11.0", 5 | "axios": "^0.27.2", 6 | "ci": "^2.2.0", 7 | "react": "^18.1.0", 8 | "react-dom": "^18.1.0", 9 | "react-redux": "^8.0.1", 10 | "react-scripts": "^5.0.1", 11 | "web-vitals": "^2.1.4" 12 | }, 13 | "scripts": { 14 | "start": "swa start http://localhost:3000 --api-location ./api --run 'react-scripts start'", 15 | "dev": "swa start http://localhost:3000 --api-location ./api --run 'react-scripts start'", 16 | "dev:install": "npm install && cd api && npm install && func init --language javascript --worker-runtime node", 17 | "build": "react-scripts build" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@azure/static-web-apps-cli": "^1.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/features/user/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit" 2 | 3 | const initialState = { 4 | userDetails: '' 5 | } 6 | 7 | export const getUserAsync = createAsyncThunk( 8 | 'user/get', 9 | async () => { 10 | // Retrieve response from /.auth/me 11 | const response = await fetch('/.auth/me'); 12 | // Convert to JSON 13 | const payload = await response.json(); 14 | // Retrieve the clientPrincipal (current user) 15 | const { clientPrincipal } = payload; 16 | if (clientPrincipal) return clientPrincipal.userDetails; 17 | else return ''; 18 | } 19 | ) 20 | 21 | export const userSlice = createSlice({ 22 | name: 'user', 23 | initialState, 24 | reducers: {}, 25 | extraReducers: (builder) => { 26 | builder.addCase(getUserAsync.fulfilled, (state, action) => { 27 | // put user into state 28 | state.userDetails = action.payload; 29 | }); 30 | } 31 | }); 32 | 33 | export const selectUserDetails = (state) => { 34 | return state.user.userDetails; 35 | } 36 | 37 | export default userSlice.reducer; -------------------------------------------------------------------------------- /src/Display.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Items } from './features/items/Items'; 4 | import { listAsync as getItems } from './features/items/itemSlice'; 5 | import { Lists } from './features/lists/Lists'; 6 | import { listAsync, selectActiveList } from './features/lists/listSlice'; 7 | 8 | function Display() { 9 | const dispatch = useDispatch(); 10 | 11 | // Load all lists 12 | useEffect(() => { 13 | dispatch(listAsync()); 14 | }, [dispatch]); 15 | 16 | // get the currently selected list 17 | const activeList = useSelector(selectActiveList); 18 | 19 | // update items for new list 20 | useEffect(() => { 21 | if (activeList && activeList.id) dispatch(getItems(activeList.id)); 22 | }, [activeList, dispatch]) 23 | 24 | return ( 25 |
26 |
27 |

Lists

28 | 29 |
30 |
31 | {activeList.name ?

{activeList.name} items

:
} 32 | 33 |
34 |
35 | ) 36 | } 37 | 38 | export default Display; -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/javascript-node-mongo 3 | // Update the VARIANT arg in docker-compose.yml to pick a Node.js version 4 | { 5 | "name": "Node.js & Mongo DB", 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/workspace", 9 | 10 | // Set *default* container specific settings.json values on container create. 11 | "settings": {}, 12 | 13 | // Add the IDs of extensions you want installed when the container is created. 14 | "extensions": [ 15 | "ms-vscode.azure-account", 16 | "dbaeumer.vscode-eslint", 17 | "mongodb.mongodb-vscode", 18 | "ms-azuretools.vscode-azurefunctions", 19 | "ms-azuretools.vscode-cosmosdb", 20 | "ms-azuretools.vscode-azurestaticwebapps" 21 | ], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | "forwardPorts": [4280, 7071, 3000], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "yarn install", 28 | 29 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 30 | "remoteUser": "node" 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | # Update 'VARIANT' to pick an LTS version of Node.js: 16, 14, 12. 10 | # Append -bullseye or -buster to pin to an OS version. 11 | # Use -bullseye variants on local arm64/Apple Silicon. 12 | VARIANT: 14-buster 13 | volumes: 14 | - ..:/workspace:cached 15 | 16 | # Overrides default command so things don't shut down after the process ends. 17 | command: sleep infinity 18 | 19 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 20 | network_mode: service:db 21 | 22 | # Uncomment the next line to use a non-root user for all processes. 23 | # user: node 24 | 25 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 26 | # (Adding the "ports" property to this file will not forward from a Codespace.) 27 | 28 | db: 29 | image: mongo:latest 30 | restart: unless-stopped 31 | volumes: 32 | - mongodb-data:/data/db 33 | 34 | # Uncomment to change startup options 35 | # environment: 36 | # MONGO_INITDB_ROOT_USERNAME: root 37 | # MONGO_INITDB_ROOT_PASSWORD: example 38 | # MONGO_INITDB_DATABASE: your-database-here 39 | 40 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 41 | # (Adding the "ports" property to this file will not forward from a Codespace.) 42 | 43 | volumes: 44 | mongodb-data: -------------------------------------------------------------------------------- /api/lists/index.js: -------------------------------------------------------------------------------- 1 | const { getUserId } = require('../core.js'); 2 | const store = require('../models/store.js'); 3 | 4 | module.exports = async function (context, req) { 5 | context.log('List function called.'); 6 | let userId = null; 7 | 8 | try { 9 | userId = getUserId(req); 10 | // If no current user, return 401 11 | if(!userId) { 12 | context.res.status = 401; 13 | return; 14 | } 15 | } catch { 16 | // Error, return unauthorized 17 | context.res.status = 401; 18 | return; 19 | } 20 | 21 | 22 | // set return value to JSON 23 | context.res = { 24 | header: { 25 | "Content-Type": "application/json" 26 | } 27 | } 28 | 29 | // connect to the database 30 | await store.connect(context.log); 31 | 32 | // Read the method and determine requested action 33 | switch (req.method) { 34 | case 'GET': 35 | // return all lists 36 | await getLists(context, userId); 37 | break; 38 | case 'POST': 39 | // create new list 40 | await createList(context, userId); 41 | break; 42 | } 43 | } 44 | 45 | async function getLists(context, userId) { 46 | const lists = await store.listStore.list(userId); 47 | context.res.body = lists; 48 | } 49 | 50 | async function createList(context, userId) { 51 | let newList = context.req.body; 52 | newList.userId = userId; 53 | newList = await store.listStore.create(newList); 54 | context.res.status = 201; 55 | context.res.body = newList; 56 | } 57 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json -------------------------------------------------------------------------------- /local-setup.md: -------------------------------------------------------------------------------- 1 | # Local setup 2 | 3 | To support running locally, the starter project is configured with a [dev container](https://code.visualstudio.com/docs/remote/create-dev-container?WT.mc_id=academic-45074-chrhar). The container has the following resources: 4 | 5 | - Node.js 6 | - Azure Functions Core Tools 7 | - MongoDB 8 | 9 | To run the project, you will need the following: 10 | 11 | - [Docker](https://docs.docker.com/engine/install/) 12 | - [Visual Studio Code](https://code.visualstudio.com?WT.mc_id=academic-45074-chrhar) 13 | - [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&WT.mc_id=academic-45074-chrhar) 14 | 15 | ## Setup 16 | 17 | 1. Clone the repository you created earlier when [deploying to Azure](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar), or [create a copy from the template](https://github.com/login?return_to=/staticwebdev/mongoose-starter/generate) and clone your copy 18 | 19 | ```bash 20 | git clone 21 | cd 22 | ``` 23 | 24 | 1. Open the project in Visual Studio Code 25 | 26 | ```bash 27 | code . 28 | ``` 29 | 30 | 1. When prompted inside Visual Studio Code, select **Reopen in Container**. The container will build, and Visual Studio Code wil refresh. 31 | 32 | 1. Inside Visual Studio Code, open a terminal window by selecting **View** > **Terminal**, and execute the following code to install the packages and run the site 33 | 34 | ```bash 35 | npm dev:install 36 | npm run dev 37 | ``` 38 | 39 | Your project will now start! 40 | 41 | 1. Navigate to [http://localhost:4280](http://localhost:4280) to use your site 42 | 43 | > **NOTE** You might be prompted to open a different port. The Azure Static Web Apps CLI will host the project on port [4280](http://localhost:4280). 44 | -------------------------------------------------------------------------------- /src/features/lists/Lists.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { saveAsync, selectLists, selectActiveList, activateList } from "./listSlice"; 4 | 5 | function DisplayList(props) { 6 | const dispatch = useDispatch(); 7 | 8 | const activeList = useSelector(selectActiveList); 9 | const [active, setActive] = useState(''); 10 | const list = props.list; 11 | 12 | useEffect(() => { 13 | setActive(list.id === activeList.id ? 'active' : ''); 14 | }, [activeList, list.id]); 15 | 16 | return ( 17 | 18 | ) 19 | } 20 | 21 | export function Lists() { 22 | const dispatch = useDispatch(); 23 | const lists = useSelector(selectLists); 24 | 25 | const [newList, setNewList] = useState({ name: '' }); 26 | 27 | function onSubmitNewList(event) { 28 | // prevent form from submitting 29 | event.preventDefault(); 30 | // save data 31 | dispatch(saveAsync(newList)); 32 | // reset item 33 | setNewList({ name: '' }); 34 | } 35 | 36 | return ( 37 |
38 |
39 | setNewList({ name: event.target.value })} value={newList.name} placeholder="create new list" /> 40 | 41 |
42 | 43 |
    44 | { lists.map(list => ) } 45 |
46 |
47 | ) 48 | } -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-kind-plant-02300391e.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - add-react-deploy 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - add-react-deploy 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build And Deploy 22 | id: builddeploy 23 | uses: Azure/static-web-apps-deploy@v1 24 | with: 25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_PLANT_02300391E }} 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 27 | action: "upload" 28 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 30 | app_location: "/" # App source code path 31 | api_location: "api" # Api source code path - optional 32 | output_location: "build" # Built app content directory - optional 33 | ###### End of Repository/Build Configurations ###### 34 | 35 | close_pull_request_job: 36 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 37 | runs-on: ubuntu-latest 38 | name: Close Pull Request Job 39 | steps: 40 | - name: Close Pull Request 41 | id: closepullrequest 42 | uses: Azure/static-web-apps-deploy@v1 43 | with: 44 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_PLANT_02300391E }} 45 | action: "close" 46 | -------------------------------------------------------------------------------- /src/features/lists/listSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { listService } from "./listAPI"; 3 | 4 | const initialState = { 5 | data: [], 6 | activeList: {}, 7 | status: 'idle' 8 | } 9 | 10 | export const listAsync = createAsyncThunk( 11 | 'lists/get', 12 | async () => { 13 | return await listService.list(); 14 | } 15 | ) 16 | 17 | export const saveAsync = createAsyncThunk( 18 | 'lists/save', 19 | async (list) => { 20 | return await listService.save(list); 21 | } 22 | ) 23 | 24 | export const listSlice = createSlice({ 25 | name: 'list', 26 | initialState, 27 | reducers: { 28 | activateList(state, action) { 29 | state.activeList = action.payload 30 | } 31 | }, 32 | extraReducers: (builder) => { 33 | builder 34 | .addCase(listAsync.fulfilled, (state, action) => { 35 | // all lists loaded 36 | // put lists into state 37 | state.data = action.payload 38 | }) 39 | .addCase(saveAsync.fulfilled, (state, action) => { 40 | // list saved 41 | // See if current list exists 42 | const existingListIndex = state.data.findIndex(l => l.id === action.payload.id); 43 | if (existingListIndex > -1) { 44 | // list exists, replace with updated list 45 | state.data[existingListIndex] = action.payload.action; 46 | } else { 47 | // list does not exist, add to end 48 | state.data.push(action.payload); 49 | state.activeList = action.payload; 50 | } 51 | }) 52 | } 53 | }) 54 | 55 | export const selectLists = (state) => { 56 | return state.lists.data; 57 | } 58 | export const selectActiveList = (state) => { 59 | return state.lists.activeList; 60 | } 61 | 62 | export default listSlice.reducer; 63 | export const { activateList } = listSlice.actions; -------------------------------------------------------------------------------- /src/features/items/itemSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; 2 | import { itemService } from "./itemAPI"; 3 | 4 | const initialState = { 5 | data: [], 6 | status: 'idle' 7 | } 8 | 9 | export const listAsync = createAsyncThunk( 10 | 'items/getList', 11 | async (data) => { 12 | return await itemService.list(data); 13 | } 14 | ) 15 | 16 | export const loadAsync = createAsyncThunk( 17 | 'items/load', 18 | async (data) => { 19 | return await itemService.load(data); 20 | } 21 | ) 22 | 23 | export const saveAsync = createAsyncThunk( 24 | 'items/save', 25 | async (data) => { 26 | return await itemService.save(data); 27 | } 28 | ) 29 | 30 | export const removeAsync = createAsyncThunk( 31 | 'items/remove', 32 | async (data) => { 33 | return await itemService.delete(data); 34 | } 35 | ) 36 | 37 | export const itemSlice = createSlice({ 38 | name: 'item', 39 | initialState, 40 | reducers: { 41 | 42 | }, 43 | extraReducers: (builder) => { 44 | builder 45 | .addCase(listAsync.fulfilled, (state, action) => { 46 | // put items into state 47 | state.data = action.payload; 48 | }) 49 | .addCase(removeAsync.fulfilled, (state, action) => { 50 | // remove item from state 51 | state.data = state.data.filter(i => i.id !== action.payload.id); 52 | }) 53 | .addCase(saveAsync.fulfilled, (state, action) => { 54 | // See if current item exists 55 | const existingItemIndex = state.data.findIndex(i => i.id === action.payload.id); 56 | if (existingItemIndex > -1) { 57 | // item exists, replace with updated item 58 | state.data[existingItemIndex] = action.payload; 59 | } 60 | else { 61 | // item does not exist, add new item to end 62 | state.data.push(action.payload); 63 | } 64 | }) 65 | }, 66 | }) 67 | 68 | export const selectListItems = (state) => { 69 | return state.items.data; 70 | } 71 | export default itemSlice.reducer; -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster 2 | ARG VARIANT=16-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # Install MongoDB command line tools if on buster and x86_64 (arm64 not supported) 6 | ARG MONGO_TOOLS_VERSION=5.0 7 | RUN . /etc/os-release \ 8 | && if [ "${VERSION_CODENAME}" = "buster" ] && [ "$(dpkg --print-architecture)" = "amd64" ]; then \ 9 | curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ 10 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian $(lsb_release -cs)/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ 11 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | && apt-get install -y mongodb-database-tools mongodb-mongosh \ 13 | && apt-get clean -y && rm -rf /var/lib/apt/lists/*; \ 14 | fi 15 | 16 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg 17 | RUN mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg 18 | RUN sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' 19 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && apt-get install azure-functions-core-tools-4 20 | # RUN sudo apt-get install azure-functions-core-tools-4 21 | 22 | # [Optional] Uncomment this section to install additional OS packages. 23 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 24 | # && apt-get -y install --no-install-recommends 25 | 26 | # [Optional] Uncomment if you want to install an additional version of node using nvm 27 | # ARG EXTRA_NODE_VERSION=10 28 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 29 | 30 | # [Optional] Uncomment if you want to install more global node modules 31 | # RUN su node -c "npm install -g " 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | React Redux App 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /api/models/store.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { getConfig } = require("../config/index.js"); 3 | const { TodoItemModel } = require("./todoItem.js"); 4 | const { TodoListModel } = require("./todoList.js"); 5 | 6 | async function configureMongoose(log) { 7 | // Configure JSON output to client 8 | // Removes version, sets _id to id 9 | mongoose.set("toJSON", { 10 | virtuals: true, 11 | versionKey: false, 12 | transform: (_, converted) => { 13 | converted.id = converted._id; 14 | delete converted._id; 15 | } 16 | }); 17 | 18 | try { 19 | const db = mongoose.connection; 20 | db.on("connecting", () => log("Mongoose connecting...")); 21 | db.on("connected", () => log("Mongoose connected successfully!")); 22 | db.on("disconnecting", () => log("Mongoose disconnecting...")); 23 | db.on("disconnected", () => log("Mongoose disconnected successfully!")); 24 | db.on("error", (err) => log("Mongoose database error:", err)); 25 | 26 | // Load configuration information 27 | const config = await getConfig(log); 28 | 29 | await mongoose.connect( 30 | config.database.connectionString, 31 | { dbName: config.database.databaseName, useNewUrlParser: true } 32 | ); 33 | } 34 | catch (err) { 35 | log(`Mongoose database error: ${err}`); 36 | throw err; 37 | } 38 | }; 39 | 40 | const itemStore = { 41 | list: async(userId, listId) => { 42 | return await TodoItemModel.find({ userId, listId }); 43 | }, 44 | create: async(item) => { 45 | return await TodoItemModel.create(item); 46 | }, 47 | update: async(id, userId, item) => { 48 | return await TodoItemModel.updateOne({ _id: id, userId}, item); 49 | }, 50 | delete: async(id, userId) => { 51 | return await TodoItemModel.deleteOne({ _id: id, userId }); 52 | } 53 | } 54 | 55 | const listStore = { 56 | list: async(userId) => { 57 | return await TodoListModel.find({ userId }).exec(); 58 | }, 59 | create: async(list) => { 60 | return await TodoListModel.create(list); 61 | }, 62 | update: async(id, userId, list) => { 63 | return await TodoListModel.updateOne({ _id: id, userId}, list).exec(); 64 | }, 65 | delete: async(id, userId) => { 66 | return await TodoListModel.deleteOne({ _id: id, userId }).exec(); 67 | } 68 | } 69 | 70 | module.exports = { 71 | connect: async(log) => { 72 | if (mongoose.connection.readyState === 1) { 73 | log('Connection already established.'); 74 | return; 75 | } 76 | 77 | await configureMongoose(log); 78 | }, 79 | itemStore, 80 | listStore 81 | } 82 | -------------------------------------------------------------------------------- /api/items/index.js: -------------------------------------------------------------------------------- 1 | const { getUserId } = require('../core.js'); 2 | const store = require('../models/store.js'); 3 | 4 | // Export our function 5 | module.exports = async function (context, req) { 6 | // Get the current user 7 | const userId = getUserId(req); 8 | 9 | // If no current user, return 401 10 | if(!userId) { 11 | context.res.status = 401; 12 | return; 13 | } 14 | 15 | // setup our default content type (we always return JSON) 16 | context.res = { 17 | header: { 18 | "Content-Type": "application/json" 19 | } 20 | } 21 | 22 | // Connect to the database 23 | await store.connect(context.log); 24 | 25 | // Read the method and determine the requested action 26 | switch (req.method) { 27 | // If get, return all tasks 28 | case 'GET': 29 | await getItems(context, userId); 30 | break; 31 | // If post, create new task 32 | case 'POST': 33 | await createItem(context, userId); 34 | break; 35 | // If put, update task 36 | case 'PUT': 37 | await updateItem(context, userId); 38 | break; 39 | case 'DELETE': 40 | await deleteItem(context, userId); 41 | break; 42 | } 43 | }; 44 | 45 | // Return all items 46 | async function getItems(context, userId) { 47 | // Get the list ID from the URL 48 | const listId = context.bindingData.listId; 49 | // load all items from database filtered by userId 50 | const items = await store.itemStore.list(userId, listId); 51 | // return all items 52 | context.res.body = items; 53 | } 54 | 55 | // Create new item 56 | async function createItem(context, userId) { 57 | // Read the uploaded item 58 | let item = context.req.body; 59 | // Add the userId 60 | item.userId = userId; 61 | // Add the listId 62 | item.listId = context.bindingData.listId; 63 | // Save to database 64 | item = await store.itemStore.create(item); 65 | context.log(item); 66 | // Set the HTTP status to created 67 | context.res.status = 201; 68 | // return new object 69 | context.res.body = item; 70 | } 71 | 72 | // Update an existing function 73 | async function updateItem(context, userId) { 74 | // Grab the id from the URL (stored in bindingData) 75 | const id = context.bindingData.id; 76 | // Get the item from the body 77 | const item = context.req.body; 78 | // Get the listId from the requeest 79 | item.listId = context.bindingData.listId; 80 | // Add the userId 81 | item.userId = userId; 82 | // Update the item in the database 83 | const result = await store.itemStore.update(id, userId, item); 84 | // Check to ensure an item was modified 85 | if (result.nModified === 1) { 86 | context.res.body = item; 87 | } else { 88 | // Item not found, status 404 89 | context.res.status = 404; 90 | } 91 | } 92 | 93 | async function deleteItem(context, userId) { 94 | const id = context.bindingData.id; 95 | const result = await store.itemStore.delete(id, userId); 96 | context.res = {id}; 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Static Web Apps - Mongoose starter 2 | 3 | This template is designed to be a starter for creating [React](https://reactjs.org) apps using [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview?WT.mc_id=academic-45074-chrhar), with [Azure Cosmos DB API for Mongo DB](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction?WT.mc_id=academic-45074-chrhar) as a database and a [Mongoose](https://mongoosejs.com/) client. It's built with the following: 4 | 5 | - Azure resources 6 | - [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview?WT.mc_id=academic-45074-chrhar) 7 | - [Azure Cosmos DB API for Mongo DB](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction?WT.mc_id=academic-45074-chrhar) 8 | - Application libraries 9 | - [React](https://reactjs.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) 10 | - [Mongoose](https://mongoosejs.com/) 11 | - [Azure Functions](https://docs.microsoft.com/azure/azure-functions/functions-overview?WT.mc_id=academic-45074-chrhar) 12 | - Development libraries 13 | - [Azure Static Web Apps CLI](https://docs.microsoft.com/azure/static-web-apps/local-development?WT.mc_id=academic-45074-chrhar) 14 | 15 | ## Azure deployment 16 | 17 | Please refer to the [documentation](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar) for information on creating the appropriate server resources and deploying the project. 18 | 19 | > **Important** 20 | > 21 | > Two environmental variables are required for the project: 22 | > 23 | > - `AZURE_COSMOS_CONNECTION_STRING`: The connection string to the database server 24 | > - `AZURE_COSMOS_DATABASE_NAME`: The name of the database 25 | > 26 | > These can be stored in [application settings](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar#configure-database-connection-string) in Azure Static Web Apps. When developing locally, the project will default to using MongoDB running on localhost. 27 | 28 | ## Local development 29 | 30 | You can run the project locally using containers by following the [local setup instructions](./local-setup.md). 31 | 32 | ## Project structure 33 | 34 | This starter project is designed to be a template for React with Redux and hosted on Azure Static Web Apps. It uses the [Redux Toolkit](https://redux-toolkit.js.org/). The project is a Todo application using authentication for Azure Static Web Apps. Todo items are collected in lists, and are scoped to individual users. To use the application: 35 | 36 | 1. Login using GitHub by clicking the login link. 37 | 1. Create a list. 38 | 1. Create todo items for the list. 39 | 40 | ### package.json scripts 41 | 42 | - **dev**: Starts the SWA CLI, Azure Functions and the React app. The application will be available on [http://localhost:4280](http://localhost:4280) 43 | 44 | ### src/app 45 | 46 | Contains [store.js](./src/app/store.js), which manages global state for the application. 47 | 48 | ### src/features 49 | 50 | Contains three "features", one each for [items](./src/features/items/), [lists](./src/features/lists/) and [user](./src/features/user/). *lists* and *items* contain a [slice](https://redux-toolkit.js.org/api/createSlice) to manage their respective state and a React component. 51 | 52 | ### api 53 | 54 | Root folder for Azure Functions. All [new serverless functions](https://docs.microsoft.com/azure/static-web-apps/add-api?tabs=react#create-the-api?WT.mc_id=academic-45074-chrhar) are added to this directory. 55 | 56 | #### api/config 57 | 58 | Contains the configuration for the database. Two environmental variables are required for the project: 59 | 60 | - `AZURE_COSMOS_CONNECTION_STRING`: The connection string to the database server 61 | - `AZURE_COSMOS_DATABASE_NAME`: The name of the database 62 | 63 | These can be stored in [application settings](https://docs.microsoft.com/azure/static-web-apps/add-mongoose?WT.mc_id=academic-45074-chrhar#configure-database-connection-string) in Azure Static Web Apps. When developing locally, the project will default to using MongoDB running on localhost. You can change the default values by updating *default.json*. 64 | 65 | #### api/models 66 | 67 | Contains the two Mongoose models, TodoItemModel and TodoListModel. It also contains *store.js*, which exposes helper functions for [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations. 68 | -------------------------------------------------------------------------------- /src/features/items/Items.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { selectActiveList } from "../lists/listSlice"; 4 | import { removeAsync, saveAsync, selectListItems } from "./itemSlice"; 5 | 6 | function ItemsDisplay(props) { 7 | const dispatch = useDispatch(); 8 | const [newItem, setNewItem] = useState({ name: '' }); 9 | 10 | function onSubmitNewItem(event) { 11 | // prevent form from submitting 12 | event.preventDefault(); 13 | // set the listId 14 | newItem.listId = props.listId; 15 | // save data 16 | dispatch(saveAsync(newItem)); 17 | // reset item 18 | setNewItem({ name: '' }); 19 | } 20 | 21 | return ( 22 |
23 |
24 | setNewItem({ name: event.target.value })} value={newItem.name} placeholder="create new item" /> 25 | 26 |
27 | 28 |
29 |
30 |

31 | 34 |

35 |
36 |
37 |
    38 | {props.activeItems.map(item => )} 39 |
40 |
41 |
42 |
43 |
44 |

45 | 48 |

49 |
50 |
51 |
    52 | {props.doneItems.map(item => )} 53 |
54 |
55 |
56 |
57 |
58 |
59 | ) 60 | } 61 | 62 | function ItemDisplay(props) { 63 | const dispatch = useDispatch(); 64 | 65 | const [editItem, setEditItem] = useState(null); 66 | 67 | function onRemove(item) { 68 | dispatch(removeAsync(item)); 69 | } 70 | 71 | function onToggleState(item) { 72 | const updatedItem = { ...item }; 73 | updatedItem.state = item.state === 'active' ? 'done' : 'active'; 74 | dispatch(saveAsync(updatedItem)); 75 | } 76 | 77 | function onStartEdit(item) { 78 | setEditItem({ ...item }); 79 | } 80 | 81 | function onEndEdit() { 82 | setEditItem(null); 83 | } 84 | 85 | function onSubmitEditForm(event) { 86 | // prevent form from submitting 87 | event.preventDefault(); 88 | // call parent event handler 89 | dispatch(saveAsync(editItem)); 90 | // end editing 91 | onEndEdit(); 92 | } 93 | 94 | if (editItem && editItem.id === props.item.id) { 95 | // Editing this item 96 | return ( 97 |
  • 98 |
    99 | setEditItem({ ...editItem, name: event.target.value })} /> 100 | 101 | 102 |
    103 |
  • 104 | ) 105 | } else { 106 | // No edit item, just display it 107 | return ( 108 |
  • 109 | onStartEdit(props.item)}>{props.item.name} 110 | 111 | 112 | 113 | 114 | 115 |
  • 116 | ) 117 | } 118 | } 119 | 120 | export function Items() { 121 | const items = useSelector(selectListItems); 122 | const activeList = useSelector(selectActiveList); 123 | 124 | const activeItems = items.filter(i => i.state === 'active'); 125 | const doneItems = items.filter(i => i.state === 'done'); 126 | if (activeList.name) { 127 | // A list is selected, display items 128 | return ( 129 | 130 | ) 131 | } else { 132 | // No list selected, display message 133 | return ( 134 |

    Select a list from the left to display items

    135 | ) 136 | } 137 | } -------------------------------------------------------------------------------- /api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/bson": { 8 | "version": "4.0.5", 9 | "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", 10 | "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", 11 | "requires": { 12 | "@types/node": "*" 13 | } 14 | }, 15 | "@types/mongodb": { 16 | "version": "3.6.20", 17 | "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", 18 | "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", 19 | "requires": { 20 | "@types/bson": "*", 21 | "@types/node": "*" 22 | } 23 | }, 24 | "@types/node": { 25 | "version": "16.10.3", 26 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", 27 | "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" 28 | }, 29 | "bl": { 30 | "version": "2.2.1", 31 | "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", 32 | "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", 33 | "requires": { 34 | "readable-stream": "^2.3.5", 35 | "safe-buffer": "^5.1.1" 36 | } 37 | }, 38 | "bluebird": { 39 | "version": "3.5.1", 40 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 41 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 42 | }, 43 | "bson": { 44 | "version": "1.1.5", 45 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", 46 | "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" 47 | }, 48 | "config": { 49 | "version": "3.3.7", 50 | "resolved": "https://registry.npmjs.org/config/-/config-3.3.7.tgz", 51 | "integrity": "sha512-mX/n7GKDYZMqvvkY6e6oBY49W8wxdmQt+ho/5lhwFDXqQW9gI+Ahp8EKp8VAbISPnmf2+Bv5uZK7lKXZ6pf1aA==", 52 | "requires": { 53 | "json5": "^2.1.1" 54 | } 55 | }, 56 | "core-util-is": { 57 | "version": "1.0.2", 58 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 59 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 60 | }, 61 | "debug": { 62 | "version": "3.1.0", 63 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 64 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 65 | "requires": { 66 | "ms": "2.0.0" 67 | }, 68 | "dependencies": { 69 | "ms": { 70 | "version": "2.0.0", 71 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 72 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 73 | } 74 | } 75 | }, 76 | "denque": { 77 | "version": "1.4.1", 78 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 79 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 80 | }, 81 | "dotenv": { 82 | "version": "16.0.1", 83 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", 84 | "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" 85 | }, 86 | "inherits": { 87 | "version": "2.0.4", 88 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 89 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 90 | }, 91 | "isarray": { 92 | "version": "1.0.0", 93 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 94 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 95 | }, 96 | "json5": { 97 | "version": "2.2.1", 98 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", 99 | "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" 100 | }, 101 | "kareem": { 102 | "version": "2.3.2", 103 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", 104 | "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==" 105 | }, 106 | "memory-pager": { 107 | "version": "1.5.0", 108 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 109 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 110 | "optional": true 111 | }, 112 | "mongodb": { 113 | "version": "3.6.11", 114 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.11.tgz", 115 | "integrity": "sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw==", 116 | "requires": { 117 | "bl": "^2.2.1", 118 | "bson": "^1.1.4", 119 | "denque": "^1.4.1", 120 | "optional-require": "^1.0.3", 121 | "safe-buffer": "^5.1.2", 122 | "saslprep": "^1.0.0" 123 | } 124 | }, 125 | "mongoose": { 126 | "version": "5.13.10", 127 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.10.tgz", 128 | "integrity": "sha512-h1tkPu/3grQjdWjoIA3uUNWvDFDcaNGwcmBLEkK9t/kzKq4L4fB2EoA/VLjQ7WGTL/pDKH6OElBTK7x/QPRvbg==", 129 | "requires": { 130 | "@types/bson": "1.x || 4.0.x", 131 | "@types/mongodb": "^3.5.27", 132 | "bson": "^1.1.4", 133 | "kareem": "2.3.2", 134 | "mongodb": "3.6.11", 135 | "mongoose-legacy-pluralize": "1.0.2", 136 | "mpath": "0.8.4", 137 | "mquery": "3.2.5", 138 | "ms": "2.1.2", 139 | "optional-require": "1.0.x", 140 | "regexp-clone": "1.0.0", 141 | "safe-buffer": "5.2.1", 142 | "sift": "13.5.2", 143 | "sliced": "1.0.1" 144 | }, 145 | "dependencies": { 146 | "optional-require": { 147 | "version": "1.0.3", 148 | "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", 149 | "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==" 150 | } 151 | } 152 | }, 153 | "mongoose-legacy-pluralize": { 154 | "version": "1.0.2", 155 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 156 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 157 | }, 158 | "mpath": { 159 | "version": "0.8.4", 160 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", 161 | "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==" 162 | }, 163 | "mquery": { 164 | "version": "3.2.5", 165 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz", 166 | "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==", 167 | "requires": { 168 | "bluebird": "3.5.1", 169 | "debug": "3.1.0", 170 | "regexp-clone": "^1.0.0", 171 | "safe-buffer": "5.1.2", 172 | "sliced": "1.0.1" 173 | }, 174 | "dependencies": { 175 | "safe-buffer": { 176 | "version": "5.1.2", 177 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 178 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 179 | } 180 | } 181 | }, 182 | "ms": { 183 | "version": "2.1.2", 184 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 185 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 186 | }, 187 | "optional-require": { 188 | "version": "1.1.8", 189 | "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", 190 | "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", 191 | "requires": { 192 | "require-at": "^1.0.6" 193 | } 194 | }, 195 | "process-nextick-args": { 196 | "version": "2.0.1", 197 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 198 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 199 | }, 200 | "readable-stream": { 201 | "version": "2.3.7", 202 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 203 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 204 | "requires": { 205 | "core-util-is": "~1.0.0", 206 | "inherits": "~2.0.3", 207 | "isarray": "~1.0.0", 208 | "process-nextick-args": "~2.0.0", 209 | "safe-buffer": "~5.1.1", 210 | "string_decoder": "~1.1.1", 211 | "util-deprecate": "~1.0.1" 212 | }, 213 | "dependencies": { 214 | "safe-buffer": { 215 | "version": "5.1.2", 216 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 217 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 218 | } 219 | } 220 | }, 221 | "regexp-clone": { 222 | "version": "1.0.0", 223 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", 224 | "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" 225 | }, 226 | "require-at": { 227 | "version": "1.0.6", 228 | "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", 229 | "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==" 230 | }, 231 | "safe-buffer": { 232 | "version": "5.2.1", 233 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 234 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 235 | }, 236 | "saslprep": { 237 | "version": "1.0.3", 238 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 239 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 240 | "optional": true, 241 | "requires": { 242 | "sparse-bitfield": "^3.0.3" 243 | } 244 | }, 245 | "sift": { 246 | "version": "13.5.2", 247 | "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz", 248 | "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA==" 249 | }, 250 | "sliced": { 251 | "version": "1.0.1", 252 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 253 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 254 | }, 255 | "sparse-bitfield": { 256 | "version": "3.0.3", 257 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 258 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 259 | "optional": true, 260 | "requires": { 261 | "memory-pager": "^1.0.2" 262 | } 263 | }, 264 | "string_decoder": { 265 | "version": "1.1.1", 266 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 267 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 268 | "requires": { 269 | "safe-buffer": "~5.1.0" 270 | }, 271 | "dependencies": { 272 | "safe-buffer": { 273 | "version": "5.1.2", 274 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 275 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 276 | } 277 | } 278 | }, 279 | "util-deprecate": { 280 | "version": "1.0.2", 281 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 282 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 283 | } 284 | } 285 | } 286 | --------------------------------------------------------------------------------