├── .gitignore ├── 2-Deploy-basic-app-to-azure ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode │ └── launch.json ├── controller │ └── rentals.controller.js ├── data │ └── fake-rentals.json ├── index.js ├── model │ └── rental.model.js ├── package.json ├── public │ └── assets │ │ └── style.css ├── readme.md ├── services │ └── httpserver.js ├── setup-in-sandbox.sh └── views │ ├── __layout │ ├── footer.html │ └── header.html │ ├── delete.html │ ├── edit.html │ ├── error.html │ ├── list.html │ └── new.html ├── 3-Add-cosmosdb-mongodb ├── .deployment ├── .devcontainer │ ├── Dockerfile │ ├── devcontainer.json │ └── docker-compose.yml ├── .env ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode │ ├── launch.json │ └── settings.json ├── controller │ └── rentals.controller.js ├── data │ └── fake-rentals.json ├── index.js ├── insert.mongodb ├── model │ └── rental.model.js ├── package.json ├── public │ └── assets │ │ └── style.css ├── readme.md ├── scripts │ └── import-data.js ├── services │ └── httpserver.js ├── setup-in-local.sh ├── setup-in-sandbox-terminal.sh ├── setup-in-sandbox.sh └── views │ ├── __layout │ ├── footer.html │ └── header.html │ ├── delete.html │ ├── edit.html │ ├── error.html │ ├── list.html │ └── new.html ├── 5-Add-blob-storage ├── .env.sample ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode │ └── launch.json ├── controller │ └── rentals.controller.js ├── data │ └── fake-rentals.json ├── index.js ├── model │ └── rental.model.js ├── package.json ├── public │ └── assets │ │ └── style.css ├── readme.md ├── services │ ├── blobstorage.js │ ├── httpserver.js │ └── imageprocessing.js └── views │ ├── __layout │ ├── footer.html │ └── header.html │ ├── delete.html │ ├── edit.html │ ├── error.html │ ├── list.html │ └── new.html ├── 90-Playwright-simple-tests ├── .env.sample ├── .github │ └── workflows │ │ └── playwright.yml ├── .gitignore ├── e2e-tests │ ├── example.spec.js │ └── rental.spec.js ├── package.json └── playwright.config.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md └── README2.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | ./**/node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | .devcontainer 3 | .github/ 4 | .vscode/** 5 | build 6 | coverage 7 | views 8 | node_modules 9 | ./packagelock-json 10 | LICENSE.md 11 | CONTRIBUTING.md 12 | CHANGELOG.md 13 | readme.md 14 | .env 15 | **/*.yml -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Mod 2", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run-script", 12 | "start" 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "pwa-node" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/controller/rentals.controller.js: -------------------------------------------------------------------------------- 1 | import { 2 | getRentals, 3 | getRentalById, 4 | deleteRentalById, 5 | addRental, 6 | updateRental, 7 | } from '../model/rental.model.js'; 8 | import createError from 'http-errors'; 9 | 10 | // List view 11 | export const viewAllRentals = async (req, res) => { 12 | const rentals = await getRentals(); 13 | 14 | res.render('list', { 15 | rentals, 16 | }); 17 | }; 18 | // List API 19 | export const apiAllRentals = async (req, res) => { 20 | const rentals = await getRentals(); 21 | res.json(rentals); 22 | }; 23 | // Delete view 24 | export const viewDeleteRental = async (req, res, next) => { 25 | const { id } = req.params; 26 | 27 | const rental = await getRentalById(id); 28 | if(!rental) return next(createError(400, 'Rental not found')); 29 | 30 | res.render('delete', { 31 | rental, 32 | }); 33 | }; 34 | // Delete API 35 | export const apiDeleteRental = async (req, res, next) => { 36 | const { id } = req.params; 37 | 38 | const rental = await getRentalById(id); 39 | if(!rental) return next(createError(400, 'Rental not found')); 40 | 41 | await deleteRentalById(id); 42 | res.redirect('/'); 43 | }; 44 | // New view 45 | export const viewAddNewRental = (req, res) => { 46 | res.render('new'); 47 | }; 48 | // New API 49 | export const apiAddNewRental = async (req, res) => { 50 | const { 51 | name, description, price, location, bedrooms, bathrooms, link, 52 | } = req.body; 53 | 54 | await addRental({ 55 | name, 56 | description, 57 | image: "https://picsum.photos/200", 58 | price, 59 | location, 60 | bedrooms, 61 | bathrooms, 62 | link, 63 | }); 64 | res.redirect('/'); 65 | }; 66 | // Edit view 67 | export const viewEditRental = async (req, res, next) => { 68 | const { id } = req.params; 69 | const rental = await getRentalById(id); 70 | if(!rental) return next(createError(400, 'Rental not found')); 71 | 72 | res.render('edit', { 73 | rental, 74 | }); 75 | }; 76 | // Edit API 77 | export const apiEditRental = async (req, res, next) => { 78 | const { 79 | name, description, price, location, bedrooms, bathrooms, link, 80 | } = req.body; 81 | const { id } = req.params; 82 | const rental = await getRentalById(id); 83 | if(!rental) return next(createError(400, 'Rental not found')); 84 | 85 | // don't update image - this will be fixed in Storage module of Learn path 86 | const updatedRental = { 87 | ...rental, 88 | name, 89 | description, 90 | price, 91 | location, 92 | bedrooms, 93 | bathrooms, 94 | link, 95 | } 96 | 97 | await updateRental(updatedRental); 98 | res.redirect('/'); 99 | }; 100 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/data/fake-rentals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Georgian Court", 4 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 5 | "image": "https://picsum.photos/200", 6 | "price": "1000000", 7 | "location": "San Francisco, CA", 8 | "bedrooms": "3", 9 | "bathrooms": "2", 10 | "link": "http://www.example.com" 11 | }, 12 | { 13 | "name": "Brittany Court", 14 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 15 | "image": "https://picsum.photos/200", 16 | "price": "3000000", 17 | "location": "San Francisco, CA", 18 | "bedrooms": "3", 19 | "bathrooms": "2", 20 | "link": "http://www.example.com" 21 | }, 22 | { 23 | "name": "Baz Homes", 24 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 25 | "image": "https://picsum.photos/200", 26 | "price": "9000000", 27 | "location": "San Francisco, CA", 28 | "bedrooms": "3", 29 | "bathrooms": "2", 30 | "link": "http://www.example.com" 31 | }, 32 | { 33 | "name": "Bar Homes", 34 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 35 | "image": "https://picsum.photos/200", 36 | "price": "4000000", 37 | "location": "San Francisco, CA", 38 | "bedrooms": "3", 39 | "bathrooms": "2", 40 | "link": "http://www.example.com" 41 | }, 42 | { 43 | "name": "Foo Homes", 44 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 45 | "image": "https://picsum.photos/200", 46 | "price": "100000", 47 | "location": "San Francisco, CA", 48 | "bedrooms": "3", 49 | "bathrooms": "2", 50 | "link": "http://www.example.com" 51 | }, 52 | { 53 | "name": "Foo Hall", 54 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 55 | "image": "https://picsum.photos/200", 56 | "price": "600000", 57 | "location": "San Francisco, CA", 58 | "bedrooms": "1", 59 | "bathrooms": "2", 60 | "link": "http://www.example.com" 61 | } 62 | ] -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import configureHttpServer from './services/httpserver.js'; 3 | 4 | // Create express app 5 | const app = express(); 6 | 7 | // Establish port 8 | const port = process.env.PORT || 8080; 9 | 10 | // Global Error Handler 11 | const onGlobalErrors = (error) => { 12 | if (error.syscall !== 'listen') { 13 | throw error; 14 | } 15 | 16 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 17 | 18 | // handle specific listen errors with friendly messages 19 | switch (error.code) { 20 | case 'EACCES': 21 | console.error(bind + ' requires elevated privileges'); 22 | process.exit(1); 23 | break; 24 | case 'EADDRINUSE': 25 | console.error(bind + ' is already in use'); 26 | process.exit(1); 27 | break; 28 | default: 29 | throw error; 30 | } 31 | }; 32 | 33 | // Create web server 34 | configureHttpServer(app).then((webserver) => { 35 | webserver.on('error', onGlobalErrors); 36 | webserver.listen(port, '0.0.0.0', () => console.log(`Server listening on port: ${port}`)); 37 | }); 38 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/model/rental.model.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | let RENTALS=[]; 5 | 6 | // Read data in when app starts 7 | // Database is kept in memory only 8 | export const connectToDatabase = async () =>{ 9 | const fakeData = JSON.parse( 10 | await readFile(new URL("../data/fake-rentals.json", import.meta.url))); 11 | 12 | for await(const item of fakeData){ 13 | await addRental(item) 14 | } 15 | } 16 | 17 | export const getRentals = () => RENTALS.sort((a, b) => b.id - a.id); 18 | 19 | export const getRentalById = (id) => 20 | RENTALS.find((rental) => rental.id === id); 21 | 22 | export const deleteRentalById = (id) => { 23 | const index = RENTALS.findIndex((rental) => rental.id === id); 24 | if (index !== -1) { 25 | RENTALS.splice(index, 1); 26 | } 27 | console.log(getRentals()); 28 | }; 29 | 30 | export const addRental = (rental) => { 31 | RENTALS.push({ 32 | id: uuidv4(), 33 | ...rental, 34 | }); 35 | console.log(getRentals()); 36 | } 37 | 38 | export const updateRental = (rental) => { 39 | const index = RENTALS.findIndex((r) => r.id === rental.id); 40 | if (index !== -1) { 41 | RENTALS[index] = { 42 | ...RENTALS[index], 43 | ...rental, 44 | }; 45 | } 46 | console.log(getRentals()); 47 | }; -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msdocs-javascript-nodejs-server", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "start": "cross-env-shell DEBUG=express:* node index.js", 7 | "format": "prettier --write ." 8 | }, 9 | "dependencies": { 10 | "body-parser": "^1.19.2", 11 | "cross-env": "^7.0.3", 12 | "ejs": "^3.1.6", 13 | "express": "^4.17.2", 14 | "http-errors": "^2.0.0", 15 | "method-override": "^3.0.0", 16 | "multer": "^1.4.4", 17 | "prettier": "^2.5.1", 18 | "uuid": "^8.3.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/public/assets/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | font-family: Helvetica, Arial, sans-serif; 7 | } 8 | 9 | .card { 10 | width: 400px; 11 | } 12 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/readme.md: -------------------------------------------------------------------------------- 1 | # JavaScript on Azure Learn Path - Module 2 - Deploying a basic app to Azure 2 | 3 | This Learn module deploys a basic app to Azure App Service. 4 | 5 | ## Requirements 6 | 7 | - Node.js LTS 8 | 9 | ## Local development 10 | 11 | - Create Azure resources 12 | - Azure App Service 13 | - Install npm dependencies: `npm install` 14 | - Start the server: `npm start` 15 | - Access Web App at: `http://127.0.0.1:8080` 16 | 17 | ## Azure portal: Configure git to push to Azure App Service 18 | 19 | 1. In the Azure portal, for your web app, select **Deployment -> Deployment Center**. 20 | 1. On the **Settings** tab, copy the **Git Clone URI**. This will become your local git value for your remote named `Azure`. 21 | 1. On the **Local Git/FTPS credentials** tab, copy the **Username** and **Password** under the application scope. These credentials allow you to deploy _only_ to this web app. 22 | 23 | ## Azure CLI: Configure git to push to Azure App Service 24 | 25 | 1. Create a user scope credential for the web app. 26 | 27 | ```azurecli 28 | az webapp deployment user set --user-name --password 29 | ``` 30 | 31 | 1. After app is created, configure deployment from local git 32 | 33 | ```azurecli 34 | az webapp deployment source config-local-git --name --resource-group 35 | ``` 36 | 37 | The output contains a URL like: https://@.scm.azurewebsites.net/.git. Use this URL to deploy your app in the next step. 38 | 39 | ## Create local git remote to Azure App Service 40 | 41 | 1. In a local terminal window, change the directory to the root of your Git repository, and add a Git remote using the URL you got from your app. If your chosen method doesn't give you a URL, use https://.scm.azurewebsites.net/.git with your app name in . 42 | 43 | ```bash 44 | git remote add azure 45 | ``` 46 | 47 | 1. Push to the Azure remote with: 48 | 49 | ```bash 50 | git push azure 51 | ``` 52 | 53 | 1. In the Git Credential Manager window, enter your user-scope or application-scope credentials, not your Azure sign-in credentials. 54 | 55 | If your Git remote URL already contains the username and password, you won't be prompted. 56 | 57 | 1. Review the output. You may see runtime-specific automation. 58 | 59 | 1. Browse to your cloud app to verify that the content is deployed: 60 | 61 | ```http 62 | http://.azurewebsites.net 63 | ``` -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/services/httpserver.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as ejs from 'ejs'; 3 | import path from 'path'; 4 | import bodyParser from 'body-parser'; 5 | import methodOverride from 'method-override'; 6 | import createError from 'http-errors'; 7 | import multer from 'multer'; 8 | import { fileURLToPath } from 'url'; 9 | import { 10 | viewAllRentals, 11 | viewDeleteRental, 12 | viewAddNewRental, 13 | viewEditRental, 14 | apiAllRentals, 15 | apiAddNewRental, 16 | apiDeleteRental, 17 | apiEditRental, 18 | } from '../controller/rentals.controller.js'; 19 | import { connectToDatabase } from '../model/rental.model.js'; 20 | 21 | const inMemoryStorage = multer.memoryStorage(); 22 | const uploadStrategy = multer({ storage: inMemoryStorage }).single('image'); 23 | 24 | // get root directory for paths 25 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 26 | 27 | const on404Error = (req, res, next) => { 28 | next(createError(404)); 29 | }; 30 | const onRouteErrors = (err, req, res, next) => { 31 | // set locals, only providing error in development 32 | res.locals.message = err.message; 33 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 34 | 35 | // render the error page 36 | res.status(err.status || 500); 37 | res.render('error', err); 38 | }; 39 | const checkTrailingSlash = (req, res, next) => { 40 | const trailingSlashUrl = req.baseUrl + req.url; 41 | if (req.originalUrl !== trailingSlashUrl) { 42 | res.redirect(301, trailingSlashUrl); 43 | } else { 44 | next(); 45 | } 46 | }; 47 | 48 | export default async (app) => { 49 | // Static files with cache 50 | app.use( 51 | express.static('public', { 52 | maxAge: '1d', 53 | cacheControl: true, 54 | }), 55 | ); 56 | 57 | // Parse JSON 58 | app.use(bodyParser.urlencoded({ extended: true })); 59 | app.use(bodyParser.json()); 60 | 61 | // Accept PATCH and DELETE verbs 62 | app.use(methodOverride('_method')); 63 | 64 | // Configure view engine to return HTML 65 | app.set('views', path.join(__dirname, '../views')); 66 | app.set('view engine', 'html'); 67 | app.engine('.html', ejs.__express); 68 | 69 | // http://localhost:3000/ instead of 70 | // http://locahost:3000 (no trailing slash) 71 | app.use(checkTrailingSlash); 72 | 73 | // EJS Views 74 | app.get('/', viewAllRentals); 75 | app.get('/rental/edit/:id', viewEditRental); 76 | app.get('/rental/delete/:id', viewDeleteRental); 77 | app.get('/rental/new', viewAddNewRental); 78 | 79 | // RESTful APIs 80 | app.get('/api/rentals', apiAllRentals); 81 | app.patch('/api/rental/:id', uploadStrategy, apiEditRental); 82 | app.delete('/api/rental/:id', apiDeleteRental); 83 | app.post('/api/rental', uploadStrategy, apiAddNewRental); 84 | 85 | // Configure error handling for routes 86 | app.use(on404Error); 87 | app.use(onRouteErrors); 88 | 89 | // Connect to Database 90 | await connectToDatabase(); 91 | 92 | return app; 93 | }; 94 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/setup-in-sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # All resources and resource group will have the same name. 4 | 5 | # Prereqs 6 | # - Must have local copy/clone of this sample repo and run this script from root of project (not root of repo) 7 | # - Must have Azure CLI installed and cached signon (az login) - Az CLI is available from Learn sandbox terminal 8 | 9 | # Learn sandbox provides 10 | # - Free `Concierge subscription` with ability to create these exact resources 11 | # - Tenant ID: 604c1504-c6a3-4080-81aa-b33091104187 12 | # - Resource group 13 | 14 | # With Bash and Azure CLI 15 | # - Create Application Insights resource 16 | # - Create App Service resource and plan, deploy sample code 17 | # - Connect App Insights to App Service 18 | 19 | # If you need to switch tenant, use this command 20 | # az login -t 604c1504-c6a3-4080-81aa-b33091104187 21 | 22 | # To Run Script use the following command on the bash terminal 23 | # bash setup-in-sandbox.sh 24 | 25 | #---------------------------------------------------------------------------------------- 26 | # DON'T CHANGE ANYTHING BELOW THIS LINE 27 | #---------------------------------------------------------------------------------------- 28 | 29 | # 30 | 31 | # Get Sandbox resource group provided for you 32 | RESOURCEGROUPSTRING=$(az group list --query "[0].name") 33 | RESOURCEGROUPNAME=`sed -e 's/^"//' -e 's/"$//' <<<"$RESOURCEGROUPSTRING" ` 34 | 35 | # Show resource group name 36 | printf '%s \n' "$RESOURCEGROUPNAME" 37 | 38 | # Silently install AZ CLI extensions if needed 39 | # on older versions 40 | echo "Allow extensions to auto-install" 41 | az config set extension.use_dynamic_install=yes_without_prompt 42 | 43 | echo "Create app insights" 44 | az monitor app-insights component create --resource-group "$RESOURCEGROUPNAME" --location westus --app js-rentals 45 | 46 | echo "Create web app and its plan" 47 | az webapp up --resource-group "$RESOURCEGROUPNAME" --location westus3 --name js-rentals --os-type "Linux" --runtime "node|14-lts" 48 | 49 | echo "Connect web app to app insights" 50 | az monitor app-insights component connect-webapp --resource-group "$RESOURCEGROUPNAME" --app js-rentals --web-app js-rentals 51 | 52 | # 53 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/__layout/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/__layout/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Azure Renter App 6 | 7 | 11 | 12 | 13 | 14 | 20 | 21 |
-------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/delete.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 | <%= rental.name %> 9 |
10 |

Do you really want to delete Rental "<%= rental.name %>"?

11 |
12 | Cancel 13 | 14 |
15 |
16 |
17 | 18 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/edit.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Update rental: "<%= rental.name %>"

4 | 5 |
6 |
7 | 8 | 19 |
20 |
21 | 22 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | $ 39 |
40 | 49 |
50 |
51 | 52 | 53 |
54 | 55 |
56 | 68 |
69 |
70 |
71 | 72 |
73 | 84 |
85 |
86 |
87 | 88 |
89 | 100 |
101 |
102 |
103 | 104 |
105 | 114 |
115 |
116 | 117 | 118 |
119 | Cancel 120 | 121 |
122 |
123 | 124 | <%- include('__layout/footer.html') %> 125 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/error.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 |
5 |

<%= status %> error: <%= message %>

6 |
7 |
8 | 9 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/list.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |

4 | <%= rentals.length %> Houses For Rent 5 | Add New Rental 8 |

9 | 10 |
13 | <% rentals.forEach(function(rental){ 14 | let formatter = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD'}); %> 15 | 16 |
17 | <%= rental.name %> 22 |
23 |

24 | <%= rental.name %> | <%= formatter.format(rental.price) %> 25 |

26 |

<%= rental.description %>

27 |

28 | <%= rental.bedrooms %>bd <%= rental.bathrooms %>ba, <%= rental.location 29 | %> 30 |

31 | 32 | <% if (rental.info) { %> 33 |

34 | Learn more 35 |

36 | <% } %> 37 | 38 | 39 | Edit 42 | Delete 45 |
46 |
47 | <% }) %> 48 |
49 | 50 | <%- include('__layout/footer.html') -%> 51 | -------------------------------------------------------------------------------- /2-Deploy-basic-app-to-azure/views/new.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Create a new rental

4 | 5 |
6 |
7 | 8 | 17 |
18 |
19 | 20 | 29 |
30 |
31 | 32 |
33 |
34 | $ 35 |
36 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 62 |
63 |
64 |
65 | 66 |
67 | 78 |
79 |
80 |
81 | 82 |
83 | 94 |
95 |
96 |
97 | 98 |
99 | 107 |
108 |
109 | 110 | 111 |
112 | Cancel 113 | 114 |
115 |
116 | 117 | <%- include('__layout/footer.html') %> 118 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.deployment: -------------------------------------------------------------------------------- 1 | SCM_DO_BUILD_DURING_DEPLOYMENT=true -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.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 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment if you want to install an additional version of node using nvm 21 | # ARG EXTRA_NODE_VERSION=10 22 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 23 | 24 | # [Optional] Uncomment if you want to install more global node modules 25 | # RUN su node -c "npm install -g " 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.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.231.6/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 | "dbaeumer.vscode-eslint", 16 | "ms-azuretools.vscode-cosmosdb", 17 | "ms-azuretools.vscode-azureappservice", 18 | "mongodb.mongodb-vscode" 19 | ], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | "forwardPorts": [8080, 27017], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "yarn install", 26 | 27 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 28 | "remoteUser": "node", 29 | "features": { 30 | "azure-cli": "latest" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.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: "16" 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 | # Uncomment the next line to use a non-root user for all processes. 22 | # user: node 23 | 24 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 25 | # (Adding the "ports" property to this file will not forward from a Codespace.) 26 | 27 | db: 28 | image: mongo:latest 29 | restart: unless-stopped 30 | volumes: 31 | - mongodb-data:/data/db 32 | # Uncomment to change startup options 33 | # environment: 34 | # MONGO_INITDB_ROOT_USERNAME: root 35 | # MONGO_INITDB_ROOT_PASSWORD: example 36 | # MONGO_INITDB_DATABASE: your-database-here 37 | 38 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 39 | # (Adding the "ports" property to this file will not forward from a Codespace.) 40 | 41 | volumes: 42 | mongodb-data: null 43 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.env: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | NODE_DEBUG=app 3 | MONGODB_URI_CONNECTION_STRING=mongodb://127.0.0.1:27017 4 | MONGODB_URI_DATABASE_NAME=js-rentals 5 | MONGODB_URI_COLLECTION_NAME=rentals 6 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | ./**/node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | .devcontainer 3 | .github/ 4 | .vscode/** 5 | build 6 | coverage 7 | views 8 | node_modules 9 | ./packagelock-json 10 | LICENSE.md 11 | CONTRIBUTING.md 12 | CHANGELOG.md 13 | readme.md 14 | .env 15 | **/*.yml -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Mod 3", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run-script", 12 | "start" 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "pwa-node" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appService.zipIgnorePattern": [ 3 | "node_modules{,/**}", 4 | ".env" 5 | ], 6 | "appService.deploySubpath": "." 7 | } 8 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/controller/rentals.controller.js: -------------------------------------------------------------------------------- 1 | import { 2 | getRentals, 3 | getRentalById, 4 | deleteRentalById, 5 | addRental, 6 | updateRental, 7 | } from '../model/rental.model.js'; 8 | import createError from 'http-errors'; 9 | 10 | // List view 11 | export const viewAllRentals = async (req, res) => { 12 | 13 | const appConnectedCorrectly = req.app.get('connected'); 14 | console.log(`App connected: ${JSON.stringify(appConnectedCorrectly)}`); 15 | 16 | if(appConnectedCorrectly.status===false) { 17 | return res.render('error', {status: 500, message: appConnectedCorrectly.err}); 18 | } 19 | 20 | const rentals = await getRentals(); 21 | 22 | res.render('list', { 23 | rentals, 24 | }); 25 | }; 26 | // List API 27 | export const apiAllRentals = async (req, res) => { 28 | const rentals = await getRentals(); 29 | res.json(rentals); 30 | }; 31 | // Delete view 32 | export const viewDeleteRental = async (req, res) => { 33 | const { id } = req.params; 34 | 35 | const rental = await getRentalById(id); 36 | if (!rental) return next(createError(400, 'Rental not found')); 37 | 38 | res.render('delete', { 39 | rental, 40 | }); 41 | 42 | }; 43 | // Delete API 44 | export const apiDeleteRental = async (req, res) => { 45 | const { id } = req.params; 46 | 47 | const rental = await getRentalById(id); 48 | if (!rental) return next(createError(400, 'Rental not found')); 49 | 50 | await deleteRentalById(id); 51 | res.redirect('/'); 52 | }; 53 | // New view 54 | export const viewAddNewRental = (req, res) => { 55 | res.render('new'); 56 | }; 57 | // New API 58 | export const apiAddNewRental = async (req, res) => { 59 | const { 60 | name, description, price, location, bedrooms, bathrooms, link, 61 | } = req.body; 62 | 63 | await addRental({ 64 | name, 65 | description, 66 | image: "https://picsum.photos/200", 67 | price, 68 | location, 69 | bedrooms, 70 | bathrooms, 71 | link, 72 | }); 73 | res.redirect('/'); 74 | }; 75 | // Edit view 76 | export const viewEditRental = async (req, res) => { 77 | const { id } = req.params; 78 | const rental = await getRentalById(id); 79 | if (!rental) return next(createError(400, 'Rental not found')); 80 | 81 | res.render('edit', { 82 | rental, 83 | }); 84 | }; 85 | // Edit API 86 | export const apiEditRental = async (req, res) => { 87 | const { 88 | name, description, price, location, bedrooms, bathrooms, link, 89 | } = req.body; 90 | const { id } = req.params; 91 | 92 | const rental = await getRentalById(id); 93 | if (!rental) return next(createError(400, 'Rental not found')); 94 | 95 | // don't update image - this will be fixed in Storage module of Learn path 96 | const updatedRental = { 97 | ...rental, 98 | name, 99 | description, 100 | price, 101 | location, 102 | bedrooms, 103 | bathrooms, 104 | link, 105 | } 106 | 107 | await updateRental(updatedRental); 108 | res.redirect('/'); 109 | 110 | }; 111 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/data/fake-rentals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Georgian Court", 4 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 5 | "image": "https://picsum.photos/200", 6 | "price": "1000000", 7 | "location": "San Francisco, CA", 8 | "bedrooms": "3", 9 | "bathrooms": "2", 10 | "link": "http://www.example.com" 11 | }, 12 | { 13 | "name": "Brittany Court", 14 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 15 | "image": "https://picsum.photos/200", 16 | "price": "3000000", 17 | "location": "San Francisco, CA", 18 | "bedrooms": "3", 19 | "bathrooms": "2", 20 | "link": "http://www.example.com" 21 | }, 22 | { 23 | "name": "Baz Homes", 24 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 25 | "image": "https://picsum.photos/200", 26 | "price": "9000000", 27 | "location": "San Francisco, CA", 28 | "bedrooms": "3", 29 | "bathrooms": "2", 30 | "link": "http://www.example.com" 31 | }, 32 | { 33 | "name": "Bar Homes", 34 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 35 | "image": "https://picsum.photos/200", 36 | "price": "4000000", 37 | "location": "San Francisco, CA", 38 | "bedrooms": "3", 39 | "bathrooms": "2", 40 | "link": "http://www.example.com" 41 | }, 42 | { 43 | "name": "Foo Homes", 44 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 45 | "image": "https://picsum.photos/200", 46 | "price": "100000", 47 | "location": "San Francisco, CA", 48 | "bedrooms": "3", 49 | "bathrooms": "2", 50 | "link": "http://www.example.com" 51 | }, 52 | { 53 | "name": "Foo Hall", 54 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 55 | "image": "https://picsum.photos/200", 56 | "price": "600000", 57 | "location": "San Francisco, CA", 58 | "bedrooms": "1", 59 | "bathrooms": "2", 60 | "link": "http://www.example.com" 61 | } 62 | ] -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import configureHttpServer from './services/httpserver.js'; 4 | 5 | // Create express app 6 | const app = express(); 7 | 8 | // Establish port 9 | const port = process.env.PORT || 8080; 10 | 11 | // Global Error Handler 12 | const onGlobalErrors = (error) => { 13 | if (error.syscall !== 'listen') { 14 | throw error; 15 | } 16 | 17 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 18 | 19 | // handle specific listen errors with friendly messages 20 | switch (error.code) { 21 | case 'EACCES': 22 | console.error(bind + ' requires elevated privileges'); 23 | process.exit(1); 24 | break; 25 | case 'EADDRINUSE': 26 | console.error(bind + ' is already in use'); 27 | process.exit(1); 28 | break; 29 | default: 30 | throw error; 31 | } 32 | }; 33 | 34 | // Create web server 35 | configureHttpServer(app).then((webserver) => { 36 | webserver.on('error', onGlobalErrors); 37 | webserver.listen(port, '0.0.0.0', () => console.log(`Server listening on port: ${port}`)); 38 | }); 39 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/insert.mongodb: -------------------------------------------------------------------------------- 1 | // MongoDB Playground 2 | // To disable this template go to Settings | MongoDB | Use Default Template For Playground. 3 | // Make sure you are connected to enable completions and to be able to run a playground. 4 | // Use Ctrl+Space inside a snippet or a string literal to trigger completions. 5 | 6 | // Select the database to use. 7 | use('js-rentals'); 8 | 9 | // The drop() command destroys all data from a collection. 10 | // Make sure you run it against the correct database and collection. 11 | db.rentals.drop(); 12 | 13 | // Insert a few documents into the rentals collection. 14 | db.rentals.insertMany([ 15 | { 16 | "name": "Georgian Court", 17 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 18 | "image": "https://picsum.photos/200", 19 | "price": "1000000", 20 | "location": "San Francisco, CA", 21 | "bedrooms": "3", 22 | "bathrooms": "2", 23 | "link": "http://www.example.com" 24 | }, 25 | { 26 | "name": "Brittany Court", 27 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 28 | "image": "https://picsum.photos/200", 29 | "price": "3000000", 30 | "location": "San Francisco, CA", 31 | "bedrooms": "3", 32 | "bathrooms": "2", 33 | "link": "http://www.example.com" 34 | }, 35 | { 36 | "name": "Baz Homes", 37 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 38 | "image": "https://picsum.photos/200", 39 | "price": "9000000", 40 | "location": "San Francisco, CA", 41 | "bedrooms": "3", 42 | "bathrooms": "2", 43 | "link": "http://www.example.com" 44 | }, 45 | { 46 | "name": "Bar Homes", 47 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 48 | "image": "https://picsum.photos/200", 49 | "price": "4000000", 50 | "location": "San Francisco, CA", 51 | "bedrooms": "3", 52 | "bathrooms": "2", 53 | "link": "http://www.example.com" 54 | }, 55 | { 56 | "name": "Foo Homes", 57 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 58 | "image": "https://picsum.photos/200", 59 | "price": "100000", 60 | "location": "San Francisco, CA", 61 | "bedrooms": "3", 62 | "bathrooms": "2", 63 | "link": "http://www.example.com" 64 | }, 65 | { 66 | "name": "Foo Hall", 67 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 68 | "image": "https://picsum.photos/200", 69 | "price": "600000", 70 | "location": "San Francisco, CA", 71 | "bedrooms": "1", 72 | "bathrooms": "2", 73 | "link": "http://www.example.com" 74 | } 75 | ]); 76 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/model/rental.model.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from 'mongodb'; 2 | 3 | const mongodbConnectionUrl = process.env.MONGODB_URI_CONNECTION_STRING; 4 | const mongodbDatabaseName = process.env.MONGODB_URI_DATABASE_NAME; 5 | const mongodbCollectionName = process.env.MONGODB_URI_COLLECTION_NAME; 6 | 7 | let client; 8 | let database; 9 | let rentalsCollection; 10 | 11 | const toJson = (data) => { 12 | // convert _id to id and clean up 13 | const idWithoutUnderscore = data._id.toString(); 14 | delete data._id; 15 | 16 | return { 17 | id: idWithoutUnderscore, 18 | ...data, 19 | }; 20 | }; 21 | 22 | // Get all rentals from database 23 | // Transform `_id` to `id` 24 | export const getRentals = async () => { 25 | const rentals = await rentalsCollection.find({}).toArray(); 26 | if (!rentals) return []; 27 | 28 | const alteredRentals = rentals.map((rental) => toJson(rental)); 29 | console.log(alteredRentals); 30 | return alteredRentals; 31 | }; 32 | // Get one rental by id 33 | export const getRentalById = async (id) => { 34 | if (!id) return null; 35 | 36 | const rental = await rentalsCollection.findOne({ _id: new ObjectId(id) }); 37 | return toJson(rental); 38 | }; 39 | // Delete one rental by id 40 | export const deleteRentalById = async (id) => { 41 | if (!id) return null; 42 | 43 | return await rentalsCollection.deleteOne({ _id: ObjectId(id) }); 44 | }; 45 | // Add one rental 46 | export const addRental = async (rental) => { 47 | return await rentalsCollection.insertOne(rental); 48 | }; 49 | // Update one rental 50 | // Only handles database, image changes are handled in controller 51 | export const updateRental = async (rental) => { 52 | return await rentalsCollection.updateOne({ _id: new ObjectId(rental.id) }, { $set: rental }); 53 | }; 54 | // Create database connection 55 | export const connectToDatabase = async () => { 56 | 57 | try{ 58 | 59 | if(!mongodbConnectionUrl || !mongodbDatabaseName || !mongodbCollectionName){ 60 | return { 61 | status: false, 62 | err: 'Missing required params to begin database connection' 63 | }; 64 | } 65 | 66 | // if not connected, go ahead and connect 67 | if (!client || !database || !rentalsCollection) { 68 | 69 | console.log("(Re)Established connection to database"); 70 | 71 | // connect 72 | client = await MongoClient.connect(mongodbConnectionUrl, { 73 | useUnifiedTopology: true, 74 | }); 75 | 76 | // get database 77 | database = client.db(mongodbDatabaseName); 78 | 79 | // create collection if it doesn't exist 80 | const collections = await database.listCollections().toArray(); 81 | const collectionExists = collections.filter((collection) => collection.name === mongodbCollectionName); 82 | if (!collectionExists) { 83 | await database.createCollection(mongodbCollectionName); 84 | } 85 | 86 | // get collection 87 | rentalsCollection = await database.collection(mongodbCollectionName); 88 | return { 89 | status: true, 90 | action: "(Re)Established connection to database" 91 | }; 92 | } else { 93 | console.log('Already connected'); 94 | return { 95 | status: true, 96 | action: "Already connected" 97 | }; 98 | } 99 | }catch(err){ 100 | console.log(err); 101 | return { 102 | status: false, 103 | err 104 | } 105 | } 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msdocs-javascript-nodejs-server", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "start:dev": "cross-env-shell DEBUG=express:* node index.js", 7 | "start": "node index.js", 8 | "import-data": "node ./scripts/import-data.js", 9 | "format": "prettier --write ." 10 | }, 11 | "dependencies": { 12 | "body-parser": "^1.19.2", 13 | "cross-env": "^7.0.3", 14 | "dotenv": "^16.0.0", 15 | "ejs": "^3.1.6", 16 | "express": "^4.17.2", 17 | "http-errors": "^2.0.0", 18 | "method-override": "^3.0.0", 19 | "mongodb": "^4.4.0", 20 | "multer": "^1.4.4", 21 | "prettier": "^2.5.1", 22 | "uuid": "^8.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/public/assets/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | font-family: Helvetica, Arial, sans-serif; 7 | } 8 | 9 | .card { 10 | width: 400px; 11 | } 12 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/readme.md: -------------------------------------------------------------------------------- 1 | # JavaScript on Azure Learn Path - Module 2 - Deploying a basic app to Azure 2 | 3 | This Learn module requires the following Azure resources to deploy correctly: 4 | 5 | * Azure App Service 6 | * Azure Cosmos DB with MongoDB API 7 | 8 | ## Requirements 9 | 10 | - Node.js LTS 11 | 12 | ## Local development 13 | 14 | - Create Azure resources 15 | - Azure App Service + Cosmos DB for MongoDB API 16 | - [Create resource](https://ms.portal.azure.com/#create/Microsoft.AppServiceWebAppDatabaseV3) in Azure portal 17 | - Create database 18 | - Create collection 19 | - Copy the following to the `.env` file: 20 | - Connection string 21 | - Database name 22 | - Collection name 23 | - Install npm dependencies: `npm install` 24 | - Verify environment variables are set in `.env` 25 | - PORT=8080 - default port for Azure App Service 26 | - MONGODB_URI_CONNECTION_STRING= 27 | - MONGODB_URI_DATABASE_NAME= 28 | - MONGODB_URI_COLLECTION_NAME= 29 | - Start the server: `npm start` 30 | - Access Web App at: `http://127.0.0.1:8080` -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/scripts/import-data.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { MongoClient } from 'mongodb'; 3 | import {readFile } from 'fs/promises'; 4 | import path from 'path'; 5 | import {fileURLToPath} from 'url'; 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | const mongodbConnectionUrl = process.env.MONGODB_URI_CONNECTION_STRING; 10 | const mongodbDatabaseName = process.env.MONGODB_URI_DATABASE_NAME; 11 | const mongodbCollectionName = process.env.MONGODB_URI_COLLECTION_NAME; 12 | 13 | const client = new MongoClient(mongodbConnectionUrl); 14 | 15 | async function importData() { 16 | 17 | // connect to database and collection 18 | await client.connect(); 19 | const db = await client.db(mongodbDatabaseName); 20 | const collection = await db.collection(mongodbCollectionName); 21 | 22 | // get data file path 23 | const fileWithPath = path.join(__dirname, '../data/fake-rentals.json'); 24 | console.log(`Read file at ${fileWithPath}`); 25 | 26 | // read data file 27 | const json = await readFile(fileWithPath, 'utf-8'); 28 | const data = JSON.parse(json) 29 | console.log(data); 30 | 31 | // insert data into collection 32 | const insertResult = await collection.insertMany(data); 33 | console.log(`Inserted documents `, insertResult); 34 | } 35 | 36 | importData().then(()=>{ 37 | console.log('done'); 38 | }).catch((err) =>{ 39 | console.log(`${err}`); 40 | }).finally(()=> client.close()) -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/services/httpserver.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as ejs from 'ejs'; 3 | import path from 'path'; 4 | import bodyParser from 'body-parser'; 5 | import methodOverride from 'method-override'; 6 | import createError from 'http-errors'; 7 | import multer from 'multer'; 8 | import { fileURLToPath } from 'url'; 9 | import { 10 | viewAllRentals, 11 | viewDeleteRental, 12 | viewAddNewRental, 13 | viewEditRental, 14 | apiAllRentals, 15 | apiAddNewRental, 16 | apiDeleteRental, 17 | apiEditRental, 18 | } from '../controller/rentals.controller.js'; 19 | import { connectToDatabase } from '../model/rental.model.js'; 20 | 21 | const inMemoryStorage = multer.memoryStorage(); 22 | const uploadStrategy = multer({ storage: inMemoryStorage }).single('image'); 23 | 24 | // get root directory for paths 25 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 26 | 27 | const on404Error = (req, res, next) => { 28 | next(createError(404)); 29 | }; 30 | const onRouteErrors = (err, req, res, next) => { 31 | // set locals, only providing error in development 32 | res.locals.message = err.message; 33 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 34 | 35 | // render the error page 36 | res.status(err.status || 500); 37 | res.render('error', err); 38 | }; 39 | const checkTrailingSlash = (req, res, next) => { 40 | const trailingSlashUrl = req.baseUrl + req.url; 41 | if (req.originalUrl !== trailingSlashUrl) { 42 | res.redirect(301, trailingSlashUrl); 43 | } else { 44 | next(); 45 | } 46 | }; 47 | 48 | export default async (app) => { 49 | // Static files with cache 50 | app.use( 51 | express.static('public', { 52 | maxAge: '1d', 53 | cacheControl: true, 54 | }), 55 | ); 56 | 57 | // Parse JSON 58 | app.use(bodyParser.urlencoded({ extended: true })); 59 | app.use(bodyParser.json()); 60 | 61 | // Accept PATCH and DELETE verbs 62 | app.use(methodOverride('_method')); 63 | 64 | // Configure view engine to return HTML 65 | app.set('views', path.join(__dirname, '../views')); 66 | app.set('view engine', 'html'); 67 | app.engine('.html', ejs.__express); 68 | 69 | // http://localhost:3000/ instead of 70 | // http://locahost:3000 (no trailing slash) 71 | app.use(checkTrailingSlash); 72 | 73 | // Connect to Database 74 | const connected = await connectToDatabase(); 75 | app.set('connected', connected); 76 | 77 | // EJS Views 78 | app.get('/', viewAllRentals); 79 | app.get('/rental/edit/:id', viewEditRental); 80 | app.get('/rental/delete/:id', viewDeleteRental); 81 | app.get('/rental/new', viewAddNewRental); 82 | 83 | // RESTful APIs 84 | app.get('/api/rentals', apiAllRentals); 85 | app.patch('/api/rental/:id', uploadStrategy, apiEditRental); 86 | app.delete('/api/rental/:id', apiDeleteRental); 87 | app.post('/api/rental', uploadStrategy, apiAddNewRental); 88 | 89 | // Configure error handling for routes 90 | app.use(on404Error); 91 | app.use(onRouteErrors); 92 | 93 | return app; 94 | }; 95 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/setup-in-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # All resources and resource group will have the same name. 4 | 5 | # Prereqs 6 | # - Must have local copy/clone of this sample repo and run this script from root of project (not root of repo) 7 | # - Must have Azure CLI installed and cached signon (az login) 8 | # - FILL IN SUBSCRIPTION 9 | 10 | # With Bash and Azure CLI 11 | # - Generate resource variables 12 | # - Create resource group 13 | # - Enable Application Insights 14 | # - Create App Service and deploy sample code in '3-Add-cosmosdb-mongodb' 15 | 16 | SUBSCRIPTION='REPLACE-WITH-YOUR-SUBSCRIPTION-ID' 17 | 18 | #---------------------------------------------------------------------------------------- 19 | # DON"T CHANGE ANYTHING BELOW THIS LINE 20 | #---------------------------------------------------------------------------------------- 21 | 22 | printf '%s \n' "$RESOURCEGROUPNAME" 23 | 24 | let "ID=$RANDOM*$RANDOM" 25 | LOCATION='westus' 26 | RESOURCENAME="js-rentals-$ID" 27 | RESOURCEGROUPNAME=$RESOURCENAME 28 | echo "resource name is $RESOURCENAME" 29 | 30 | OSTYPE="Linux" 31 | RUNTIME="node|14-lts" 32 | 33 | # Silently install AZ CLI extensions if needed 34 | # on older versions 35 | echo "Allow extensions to auto-install" 36 | az config set extension.use_dynamic_install=yes_without_prompt 37 | 38 | # - Create resource group 39 | echo "Create resource group: $RESOURCEGROUPNAME" 40 | az group create --subscription $SUBSCRIPTION --name $RESOURCENAME --location $LOCATION 41 | 42 | # Create Azure Workspace for log analytics 43 | #echo "Create log-analytics workspace" 44 | #az monitor log-analytics workspace create --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --location $LOCATION --workspace-name $RESOURCENAME --query-access disabled --ingestion-access disabled 45 | 46 | # - Create Monitor (Application Insights) 47 | # az monitor app-insights component create --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --location $LOCATION --app jimb-rentals-with-data-83422584 --workspace "/subscriptions/320d9379-a62c-4b5d-84ab-52f2b0fc40ac/resourcegroups/jimb-rentals-with-data-83422584/providers/microsoft.operationalinsights/workspaces/jimb-rentals-with-data-83422584" 48 | # WORKS - az monitor app-insights component create --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --location $LOCATION --app jimb-rentals-with-data-83422584 --workspace jimb-rentals-with-data-83422584 49 | echo "Create app insights" 50 | az monitor app-insights component create --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --location $LOCATION --app $RESOURCENAME #--workspace $RESOURCENAME 51 | 52 | # - Create App Service 53 | # 54 | # - Create a webapp and deploy code from a local workspace to the app. 55 | # - The command is required to run from the folder where the code is present. 56 | # 57 | # - add --debug to see verbose messages 58 | echo "Create web app" 59 | az webapp up --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --location $LOCATION --name $RESOURCENAME --os-type $OSTYPE --runtime $RUNTIME #--debug 60 | 61 | # - Connect to App Service 62 | echo "Connect web app to app insights" 63 | az monitor app-insights component connect-webapp --subscription $SUBSCRIPTION --resource-group $RESOURCEGROUPNAME --app $RESOURCENAME --web-app $RESOURCENAME -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/setup-in-sandbox-terminal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # All resources and resource group will have the same name. 4 | 5 | # Prereqs 6 | # - Must have local copy/clone of this sample repo and run this script from root of project (not root of repo) 7 | # - Must have Azure CLI installed and cached signon (az login) 8 | 9 | # With Bash and Azure CLI 10 | # - Create Application Insights resource 11 | # - Create App Service resource, deploy sample code 12 | # - Connect App Insights to App Service 13 | 14 | #---------------------------------------------------------------------------------------- 15 | # DON'T CHANGE ANYTHING BELOW THIS LINE 16 | #---------------------------------------------------------------------------------------- 17 | 18 | # az login -t 604c1504-c6a3-4080-81aa-b33091104187 19 | 20 | RESOURCEGROUPSTRING=$(az group list --query "[0].name") 21 | RESOURCEGROUPNAME=`sed -e 's/^"//' -e 's/"$//' <<<"$RESOURCEGROUPSTRING" ` 22 | 23 | printf '%s \n' "$RESOURCEGROUPNAME" 24 | 25 | # Silently install AZ CLI extensions if needed 26 | # on older versions 27 | echo "Allow extensions to auto-install" 28 | az config set extension.use_dynamic_install=yes_without_prompt 29 | 30 | echo "Create app insights" 31 | az monitor app-insights component create --resource-group "$RESOURCEGROUPNAME" --location westus --app js-rentals 32 | 33 | echo "Create app plan" 34 | az appservice plan create --resource-group "$RESOURCEGROUPNAME" --name js-rentals --location westus3 --sku F1 --is-linux 35 | 36 | echo "Create web app" 37 | az webapp create --resource-group "$RESOURCEGROUPNAME" --location westus3 --name js-rentals --os-type "Linux" --runtime "node|14-lts" 38 | 39 | echo "Connect web app to app insights" 40 | az monitor app-insights component connect-webapp --resource-group "$RESOURCEGROUPNAME" --app js-rentals --web-app js-rentals 41 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/setup-in-sandbox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # All resources and resource group will have the same name. 4 | 5 | # Prereqs 6 | # - Must have local copy/clone of this sample repo and run this script from root of project (not root of repo) 7 | # - Must have Azure CLI installed and cached signon (az login) 8 | 9 | # With Bash and Azure CLI 10 | # - Create Application Insights resource 11 | # - Create App Service resource, deploy sample code 12 | # - Connect App Insights to App Service 13 | 14 | #---------------------------------------------------------------------------------------- 15 | # DON'T CHANGE ANYTHING BELOW THIS LINE 16 | #---------------------------------------------------------------------------------------- 17 | 18 | # az login -t 604c1504-c6a3-4080-81aa-b33091104187 19 | 20 | RESOURCEGROUPSTRING=$(az group list --query "[0].name") 21 | RESOURCEGROUPNAME=`sed -e 's/^"//' -e 's/"$//' <<<"$RESOURCEGROUPSTRING" ` 22 | 23 | printf '%s \n' "$RESOURCEGROUPNAME" 24 | 25 | # Silently install AZ CLI extensions if needed 26 | # on older versions 27 | echo "Allow extensions to auto-install" 28 | az config set extension.use_dynamic_install=yes_without_prompt 29 | 30 | echo "Create app insights" 31 | az monitor app-insights component create --resource-group "$RESOURCEGROUPNAME" --location westus --app js-rentals 32 | 33 | echo "Create web app" 34 | az webapp up --resource-group "$RESOURCEGROUPNAME" --location westus3 --name js-rentals --os-type "Linux" --runtime "node|14-lts" 35 | 36 | echo "Connect web app to app insights" 37 | az monitor app-insights component connect-webapp --resource-group "$RESOURCEGROUPNAME" --app js-rentals --web-app js-rentals -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/__layout/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/__layout/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Azure Renter App 6 | 7 | 11 | 12 | 13 | 14 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/delete.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 | <%= rental.name %> 9 |
10 |

Do you really want to delete Rental "<%= rental.name %>"?

11 |
12 | Cancel 13 | 14 |
15 |
16 |
17 | 18 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/edit.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Update rental: "<%= rental.name %>"

4 | 5 |
6 |
7 | 8 | 19 |
20 |
21 | 22 | 32 |
33 |
34 | 35 | 36 |
37 |
38 | $ 39 |
40 | 49 |
50 |
51 | 52 | 53 |
54 | 55 |
56 | 68 |
69 |
70 |
71 | 72 |
73 | 84 |
85 |
86 |
87 | 88 |
89 | 100 |
101 |
102 |
103 | 104 |
105 | 114 |
115 |
116 | 117 | 118 |
119 | Cancel 120 | 121 |
122 |
123 | 124 | <%- include('__layout/footer.html') %> 125 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/error.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 |
5 |

<%= status %> error: <%= message %>

6 |
7 |
8 | 9 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/list.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |

4 | <%= rentals.length %> Houses For Rent 5 | Add New Rental 8 |

9 | 10 |
13 | <% rentals.forEach(function(rental){ 14 | let formatter = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD'}); %> 15 | 16 |
17 | <%= rental.name %> 22 |
23 |

24 | <%= rental.name %> | <%= formatter.format(rental.price) %> 25 |

26 |

<%= rental.description %>

27 |

28 | <%= rental.bedrooms %>bd <%= rental.bathrooms %>ba, <%= rental.location 29 | %> 30 |

31 | 32 | <% if (rental.info) { %> 33 |

34 | Learn more 35 |

36 | <% } %> 37 | 38 | 39 | Edit 42 | Delete 45 |
46 |
47 | <% }) %> 48 |
49 | 50 | <%- include('__layout/footer.html') -%> 51 | -------------------------------------------------------------------------------- /3-Add-cosmosdb-mongodb/views/new.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Create a new rental

4 | 5 |
6 |
7 | 8 | 17 |
18 |
19 | 20 | 29 |
30 |
31 | 32 |
33 |
34 | $ 35 |
36 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 62 |
63 |
64 |
65 | 66 |
67 | 78 |
79 |
80 |
81 | 82 |
83 | 94 |
95 |
96 |
97 | 98 |
99 | 107 |
108 |
109 | 110 | 111 |
112 | Cancel 113 | 114 |
115 |
116 | 117 | <%- include('__layout/footer.html') %> 118 | -------------------------------------------------------------------------------- /5-Add-blob-storage/.env.sample: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | NODE_DEBUG=app 3 | MONGODB_URI_CONNECTION_STRING= 4 | MONGODB_URI_DATABASE_NAME= 5 | MONGODB_URI_COLLECTION_NAME= 6 | AZURE_STORAGE_BLOB_CONNECTION_STRING= 7 | AZURE_STORAGE_BLOB_CONTAINER_NAME=rental -------------------------------------------------------------------------------- /5-Add-blob-storage/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | ./**/node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /5-Add-blob-storage/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | .devcontainer 3 | .github/ 4 | .vscode/** 5 | build 6 | coverage 7 | views 8 | node_modules 9 | ./packagelock-json 10 | LICENSE.md 11 | CONTRIBUTING.md 12 | CHANGELOG.md 13 | readme.md 14 | .env 15 | **/*.yml -------------------------------------------------------------------------------- /5-Add-blob-storage/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /5-Add-blob-storage/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Mod 5", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run-script", 12 | "start" 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "pwa-node" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /5-Add-blob-storage/controller/rentals.controller.js: -------------------------------------------------------------------------------- 1 | import createError from 'http-errors'; 2 | import { 3 | getRentals, 4 | getRentalById, 5 | deleteRentalById, 6 | addRental, 7 | updateRental, 8 | } from '../model/rental.model.js'; 9 | import { deleteBlob, uploadBlob } from '../services/blobstorage.js'; 10 | 11 | 12 | // List view 13 | export const viewAllRentals = async (req, res) => { 14 | const rentals = await getRentals(); 15 | 16 | res.render('list', { 17 | rentals, 18 | }); 19 | }; 20 | // List API 21 | export const apiAllRentals = async (req, res) => { 22 | const rentals = await getRentals(); 23 | res.json(rentals); 24 | }; 25 | // Delete view 26 | export const viewDeleteRental = async (req, res) => { 27 | const { id } = req.params; 28 | 29 | const rental = await getRentalById(id); 30 | if(!rental) return next(createError(400, 'Rental not found')); 31 | 32 | res.render('delete', { 33 | rental, 34 | }); 35 | }; 36 | // Delete API 37 | export const apiDeleteRental = async (req, res) => { 38 | const { id } = req.params; 39 | 40 | const rental = await getRentalById(id); 41 | if(!rental) return next(createError(400, 'Rental not found')); 42 | 43 | await deleteBlob(rental.image); 44 | await deleteRentalById(id); 45 | res.redirect('/'); 46 | }; 47 | // New view 48 | export const viewAddNewRental = (req, res) => { 49 | res.render('new'); 50 | }; 51 | // New API 52 | export const apiAddNewRental = async (req, res) => { 53 | const { 54 | name, description, price, location, bedrooms, bathrooms, link, 55 | } = req.body; 56 | let imageBlob = null; 57 | 58 | if (req.file) { 59 | imageBlob = await uploadBlob(req.file); 60 | } 61 | 62 | await addRental({ 63 | name, 64 | description, 65 | image: imageBlob && imageBlob.image ? imageBlob.image : null, 66 | price, 67 | location, 68 | bedrooms, 69 | bathrooms, 70 | link, 71 | }); 72 | res.redirect('/'); 73 | }; 74 | // Edit view 75 | export const viewEditRental = async (req, res) => { 76 | const { id } = req.params; 77 | 78 | const rental = await getRentalById(id); 79 | if(!rental) return next(createError(400, 'Rental not found')); 80 | 81 | res.render('edit', { 82 | rental, 83 | }); 84 | }; 85 | // Edit API 86 | export const apiEditRental = async (req, res) => { 87 | const { 88 | name, description, price, location, bedrooms, bathrooms, link, 89 | } = req.body; 90 | const { id } = req.params; 91 | 92 | const rental = await getRentalById(id); 93 | if(!rental) return next(createError(400, 'Rental not found')); 94 | 95 | let imageBlob = null; 96 | 97 | if (req.file) { 98 | await deleteBlob(rental.image); 99 | imageBlob = await uploadBlob(req.file); 100 | } 101 | 102 | // don't update image - this will be fixed in Storage module of Learn path 103 | const updatedRental = { 104 | ...rental, 105 | name, 106 | description, 107 | image: imageBlob && imageBlob.image ? imageBlob.image : rental.image, 108 | price, 109 | location, 110 | bedrooms, 111 | bathrooms, 112 | link, 113 | } 114 | 115 | await updateRental(updatedRental); 116 | res.redirect('/'); 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /5-Add-blob-storage/data/fake-rentals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Georgian Court", 4 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 5 | "image": "", 6 | "price": "1000000", 7 | "location": "San Francisco, CA", 8 | "bedrooms": "3", 9 | "bathrooms": "2", 10 | "link": "http://www.example.com" 11 | }, 12 | { 13 | "name": "Brittany Court", 14 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 15 | "image": "", 16 | "price": "3000000", 17 | "location": "San Francisco, CA", 18 | "bedrooms": "3", 19 | "bathrooms": "2", 20 | "link": "http://www.example.com" 21 | }, 22 | { 23 | "name": "Baz Homes", 24 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 25 | "image": "", 26 | "price": "9000000", 27 | "location": "San Francisco, CA", 28 | "bedrooms": "3", 29 | "bathrooms": "2", 30 | "link": "http://www.example.com" 31 | }, 32 | { 33 | "name": "Bar Homes", 34 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 35 | "image": "", 36 | "price": "4000000", 37 | "location": "San Francisco, CA", 38 | "bedrooms": "3", 39 | "bathrooms": "2", 40 | "link": "http://www.example.com" 41 | }, 42 | { 43 | "name": "Foo Homes", 44 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 45 | "image": "", 46 | "price": "100000", 47 | "location": "San Francisco, CA", 48 | "bedrooms": "3", 49 | "bathrooms": "2", 50 | "link": "http://www.example.com" 51 | }, 52 | { 53 | "name": "Foo Hall", 54 | "description": "lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quaerat.", 55 | "image": "", 56 | "price": "600000", 57 | "location": "San Francisco, CA", 58 | "bedrooms": "1", 59 | "bathrooms": "2", 60 | "link": "http://www.example.com" 61 | } 62 | ] -------------------------------------------------------------------------------- /5-Add-blob-storage/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import configureHttpServer from './services/httpserver.js'; 4 | 5 | // Create express app 6 | const app = express(); 7 | 8 | // Establish port 9 | const port = process.env.PORT || 8080; 10 | 11 | // Global Error Handler 12 | const onGlobalErrors = (error) => { 13 | if (error.syscall !== 'listen') { 14 | throw error; 15 | } 16 | 17 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 18 | 19 | // handle specific listen errors with friendly messages 20 | switch (error.code) { 21 | case 'EACCES': 22 | console.error(bind + ' requires elevated privileges'); 23 | process.exit(1); 24 | break; 25 | case 'EADDRINUSE': 26 | console.error(bind + ' is already in use'); 27 | process.exit(1); 28 | break; 29 | default: 30 | throw error; 31 | } 32 | }; 33 | 34 | // Create web server 35 | configureHttpServer(app).then((webserver) => { 36 | webserver.on('error', onGlobalErrors); 37 | webserver.listen(port, '0.0.0.0', () => console.log(`Server listening on port: ${port}`)); 38 | }); 39 | -------------------------------------------------------------------------------- /5-Add-blob-storage/model/rental.model.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from 'mongodb'; 2 | 3 | const mongodbConnectionUrl = process.env.MONGODB_URI_CONNECTION_STRING; 4 | const mongodbDatabaseName = process.env.MONGODB_URI_DATABASE_NAME; 5 | const mongodbCollectionName = process.env.MONGODB_URI_COLLECTION_NAME; 6 | 7 | if(!mongodbConnectionUrl 8 | || !mongodbDatabaseName 9 | || !mongodbCollectionName) throw new Error("Missing MongoDB connection information"); 10 | 11 | let client; 12 | let database; 13 | let rentalsCollection; 14 | 15 | const toJson = (data) => { 16 | // convert _id to id and clean up 17 | const idWithoutUnderscore = data._id.toString(); 18 | delete data._id; 19 | 20 | return { 21 | id: idWithoutUnderscore, 22 | ...data, 23 | }; 24 | }; 25 | 26 | // Get all rentals from database 27 | // Transform `_id` to `id` 28 | export const getRentals = async () => { 29 | const rentals = await rentalsCollection.find({}).toArray(); 30 | if (!rentals) return []; 31 | 32 | const alteredRentals = rentals.map((rental) => toJson(rental)); 33 | console.log(alteredRentals); 34 | return alteredRentals; 35 | }; 36 | // Get one rental by id 37 | export const getRentalById = async (id) => { 38 | if (!id) return null; 39 | 40 | const rental = await rentalsCollection.findOne({ _id: new ObjectId(id) }); 41 | return toJson(rental); 42 | }; 43 | // Delete one rental by id 44 | export const deleteRentalById = async (id) => { 45 | if (!id) return null; 46 | 47 | return await rentalsCollection.deleteOne({ _id: ObjectId(id) }); 48 | }; 49 | // Add one rental 50 | export const addRental = async (rental) => { 51 | return await rentalsCollection.insertOne(rental); 52 | }; 53 | // Update one rental 54 | // Only handles database, image changes are handled in controller 55 | export const updateRental = async (rental) => { 56 | return await rentalsCollection.updateOne({ _id: rental.id }, { $set: rental }); 57 | }; 58 | // Create database connection 59 | export const connectToDatabase = async () => { 60 | 61 | if (!client || !database || !rentalsCollection) { 62 | // connect 63 | client = await MongoClient.connect(mongodbConnectionUrl, { 64 | useUnifiedTopology: true, 65 | }); 66 | 67 | // get database 68 | database = client.db(mongodbDatabaseName); 69 | 70 | // create collection if it doesn't exist 71 | const collections = await database.listCollections().toArray(); 72 | const collectionExists = collections.filter((collection) => collection.name === mongodbCollectionName); 73 | if (!collectionExists) { 74 | await database.createCollection(mongodbCollectionName); 75 | } 76 | 77 | // get collection 78 | rentalsCollection = await database.collection(mongodbCollectionName); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /5-Add-blob-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msdocs-javascript-nodejs-server", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "start": "cross-env-shell DEBUG=express:* node index.js", 7 | "format": "prettier --write ." 8 | }, 9 | "dependencies": { 10 | "@azure/storage-blob": "^12.9.0", 11 | "body-parser": "^1.19.2", 12 | "cross-env": "^7.0.3", 13 | "dotenv": "^16.0.0", 14 | "ejs": "^3.1.6", 15 | "express": "^4.17.2", 16 | "http-errors": "^2.0.0", 17 | "into-stream": "^7.0.0", 18 | "method-override": "^3.0.0", 19 | "mongodb": "^4.4.0", 20 | "multer": "^1.4.4", 21 | "prettier": "^2.5.1", 22 | "sharp": "^0.30.2", 23 | "uuid": "^8.3.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /5-Add-blob-storage/public/assets/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | font-family: Helvetica, Arial, sans-serif; 7 | } 8 | 9 | .card { 10 | width: 400px; 11 | } 12 | -------------------------------------------------------------------------------- /5-Add-blob-storage/readme.md: -------------------------------------------------------------------------------- 1 | # JavaScript on Azure Learn Path - Module 2 - Deploying a basic app to Azure 2 | 3 | This Learn module requires the following Azure resources to deploy correctly: 4 | 5 | * Azure App Service 6 | * Azure Cosmos DB with MongoDB API 7 | 8 | ## Requirements 9 | 10 | - Node.js LTS 11 | 12 | ## Local development 13 | 14 | - Create Azure resources 15 | - Azure App Service + Cosmos DB for MongoDB API 16 | - [Create resource](https://ms.portal.azure.com/#create/Microsoft.AppServiceWebAppDatabaseV3) in Azure portal 17 | - Create database 18 | - Create collection 19 | - Copy the following to the `.env` file: 20 | - Connection string 21 | - Database name 22 | - Collection name 23 | - Install npm dependencies: `npm install` 24 | - Verify environment variables are set in `.env` 25 | - PORT=8080 - default port for Azure App Service 26 | - MONGODB_URI_CONNECTION_STRING= 27 | - MONGODB_URI_DATABASE_NAME= 28 | - MONGODB_URI_COLLECTION_NAME= 29 | - Start the server: `npm start` 30 | - Access Web App at: `http://127.0.0.1:8080` -------------------------------------------------------------------------------- /5-Add-blob-storage/services/blobstorage.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { BlobServiceClient, BlockBlobClient } from '@azure/storage-blob'; 3 | import path from 'path'; 4 | import getStream from 'into-stream'; 5 | import { generateThumbnailImage } from './imageprocessing.js'; 6 | 7 | // Azure Storage Connection String 8 | const azureStorageConnectionUrl = process.env.AZURE_STORAGE_BLOB_CONNECTION_STRING; 9 | 10 | // Azure Storage Container name 11 | // all images are stored in same container 12 | const azureContainerName = process.env.AZURE_STORAGE_BLOB_CONTAINER_NAME; 13 | 14 | if(!azureStorageConnectionUrl 15 | || !azureContainerName) throw new Error("Missing Blob Storage connection information"); 16 | 17 | let azureContainerClient = null; 18 | let azureBlobServiceClient = null; 19 | 20 | export const connectToBlobStorage = async () => { 21 | azureBlobServiceClient = BlobServiceClient.fromConnectionString(azureStorageConnectionUrl); 22 | azureContainerClient = await azureBlobServiceClient.getContainerClient(azureContainerName); 23 | 24 | // public blob access 25 | // no public container enumeration 26 | await azureContainerClient.createIfNotExists({access: "blob"}); 27 | }; 28 | 29 | const writeStreamToAzureStorage = async (fileName, imageBuffer) => { 30 | // create blob client 31 | const blobFile = new BlockBlobClient( 32 | azureStorageConnectionUrl, 33 | azureContainerName, 34 | fileName, 35 | ); 36 | 37 | // convert buffer to stream 38 | const stream = getStream(imageBuffer.buffer); 39 | const streamLength = imageBuffer.buffer.length; 40 | 41 | // upload stream as Azure Blob Storage 42 | await blobFile.uploadStream(stream, streamLength); 43 | 44 | // return name and url 45 | return blobFile; 46 | }; 47 | 48 | export const uploadBlob = async (image) => { 49 | if (!image) return null; 50 | 51 | const { ext } = path.parse(image.originalname); 52 | 53 | // only images with these file 54 | // extensions are allowed 55 | if ( 56 | !ext === '.jpg' 57 | || !ext === '.png' 58 | || !ext === '.gif' 59 | || !ext === '.webp' 60 | || !ext === '.avif') return null; 61 | 62 | const thumbnailBuffer = await generateThumbnailImage(image); 63 | 64 | // file name of image is a GUID, which provides 65 | // - obfuscation 66 | // - collision avoidance 67 | const thumbnailBlobFile = await writeStreamToAzureStorage( 68 | uuidv4(), 69 | thumbnailBuffer, 70 | ); 71 | 72 | return { image: thumbnailBlobFile.url }; 73 | }; 74 | 75 | export const deleteBlob = async (url) => { 76 | // determine file name of image from url 77 | const parts = url.split('/'); 78 | const fileName = parts[parts.length - 1]; 79 | 80 | if(!fileName) return; 81 | 82 | // create blob client 83 | const blobFile = new BlockBlobClient( 84 | azureStorageConnectionUrl, 85 | azureContainerName, 86 | fileName, 87 | ); 88 | 89 | if(blobFile && blobFile.url){ 90 | // delete image 91 | await azureContainerClient.deleteBlob(fileName); 92 | } 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /5-Add-blob-storage/services/httpserver.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as ejs from 'ejs'; 3 | import path from 'path'; 4 | import bodyParser from 'body-parser'; 5 | import methodOverride from 'method-override'; 6 | import createError from 'http-errors'; 7 | import multer from 'multer'; 8 | import { fileURLToPath } from 'url'; 9 | import { 10 | viewAllRentals, 11 | viewDeleteRental, 12 | viewAddNewRental, 13 | viewEditRental, 14 | apiAllRentals, 15 | apiAddNewRental, 16 | apiDeleteRental, 17 | apiEditRental, 18 | } from '../controller/rentals.controller.js'; 19 | import { connectToDatabase } from '../model/rental.model.js'; 20 | import { connectToBlobStorage } from '../services/blobstorage.js'; 21 | 22 | 23 | const inMemoryStorage = multer.memoryStorage(); 24 | const uploadStrategy = multer({ storage: inMemoryStorage }).single('image'); 25 | 26 | // get root directory for paths 27 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 28 | 29 | const on404Error = (req, res, next) => { 30 | next(createError(404)); 31 | }; 32 | const onRouteErrors = (err, req, res, next) => { 33 | // set locals, only providing error in development 34 | res.locals.message = err.message; 35 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 36 | 37 | // render the error page 38 | res.status(err.status || 500); 39 | res.render('error', err); 40 | }; 41 | const checkTrailingSlash = (req, res, next) => { 42 | const trailingSlashUrl = req.baseUrl + req.url; 43 | if (req.originalUrl !== trailingSlashUrl) { 44 | res.redirect(301, trailingSlashUrl); 45 | } else { 46 | next(); 47 | } 48 | }; 49 | 50 | export default async (app) => { 51 | // Static files with cache 52 | app.use( 53 | express.static('public', { 54 | maxAge: '1d', 55 | cacheControl: true, 56 | }), 57 | ); 58 | 59 | // Parse JSON 60 | app.use(bodyParser.urlencoded({ extended: true })); 61 | app.use(bodyParser.json()); 62 | 63 | // Accept PATCH and DELETE verbs 64 | app.use(methodOverride('_method')); 65 | 66 | // Configure view engine to return HTML 67 | app.set('views', path.join(__dirname, '../views')); 68 | app.set('view engine', 'html'); 69 | app.engine('.html', ejs.__express); 70 | 71 | // http://localhost:3000/ instead of 72 | // http://locahost:3000 (no trailing slash) 73 | app.use(checkTrailingSlash); 74 | 75 | // EJS Views 76 | app.get('/', viewAllRentals); 77 | app.get('/rental/edit/:id', viewEditRental); 78 | app.get('/rental/delete/:id', viewDeleteRental); 79 | app.get('/rental/new', viewAddNewRental); 80 | 81 | // RESTful APIs 82 | app.get('/api/rentals', apiAllRentals); 83 | app.patch('/api/rental/:id', uploadStrategy, apiEditRental); 84 | app.delete('/api/rental/:id', apiDeleteRental); 85 | app.post('/api/rental', uploadStrategy, apiAddNewRental); 86 | 87 | // Configure error handling for routes 88 | app.use(on404Error); 89 | app.use(onRouteErrors); 90 | 91 | // Connect to Database 92 | await connectToDatabase(); 93 | 94 | // Connect to Blob Storage 95 | await connectToBlobStorage(); 96 | 97 | return app; 98 | }; 99 | -------------------------------------------------------------------------------- /5-Add-blob-storage/services/imageprocessing.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | 3 | // Convert original image into standard size 4 | // for this app 5 | // in PNG format 6 | export const generateThumbnailImage = async (image) => { 7 | if (!image) return null; 8 | 9 | const width = 400; 10 | const height = 400; 11 | 12 | return await sharp(image.buffer).resize(width, height).png().toBuffer(); 13 | }; -------------------------------------------------------------------------------- /5-Add-blob-storage/views/__layout/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /5-Add-blob-storage/views/__layout/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Azure Renter App 6 | 7 | 11 | 12 | 13 | 14 | 20 | 21 |
-------------------------------------------------------------------------------- /5-Add-blob-storage/views/delete.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 | <% if (rental.image) { %> 5 | <%= rental.name %> 10 | <% } %> 11 |
12 |

Do you really want to delete Rental "<%= rental.name %>"?

13 |
14 | Cancel 15 | 16 |
17 |
18 |
19 | 20 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /5-Add-blob-storage/views/edit.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Update rental: "<%= rental.name %>"

4 | 5 |
6 |
7 | 8 | 19 |
20 |
21 | 22 | 32 |
33 | <% if (rental.image) { %> 34 | <%= rental.name %> 35 | <% } %> 36 |
37 | 38 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | $ 53 |
54 | 63 |
64 |
65 | 66 | 67 |
68 | 69 |
70 | 82 |
83 |
84 |
85 | 86 |
87 | 98 |
99 |
100 |
101 | 102 |
103 | 114 |
115 |
116 |
117 | 118 |
119 | 128 |
129 |
130 | 131 | 132 |
133 | Cancel 134 | 135 |
136 |
137 | 138 | <%- include('__layout/footer.html') %> 139 | -------------------------------------------------------------------------------- /5-Add-blob-storage/views/error.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |
4 |
5 |

<%= status %> error: <%= message %>

6 |
7 |
8 | 9 | <%- include('__layout/footer.html') -%> -------------------------------------------------------------------------------- /5-Add-blob-storage/views/list.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') -%> 2 | 3 |

4 | <%= rentals.length %> Houses For Rent 5 | Add New Rental 8 |

9 | 10 |
13 | <% rentals.forEach(function(rental){ 14 | let formatter = new Intl.NumberFormat('en-US', {style: 'currency',currency: 'USD'}); %> 15 | 16 |
17 | <% if (rental.image) { %> 18 | <%= rental.name %> 23 | <% } %> 24 |
25 |

26 | <%= rental.name %> | <%= formatter.format(rental.price) %> 27 |

28 |

<%= rental.description %>

29 |

30 | <%= rental.bedrooms %>bd <%= rental.bathrooms %>ba, <%= rental.location 31 | %> 32 |

33 | 34 | <% if (rental.info) { %> 35 |

36 | Learn more 37 |

38 | <% } %> 39 | 40 | 41 | Edit 44 | Delete 47 |
48 |
49 | <% }) %> 50 |
51 | 52 | <%- include('__layout/footer.html') -%> 53 | -------------------------------------------------------------------------------- /5-Add-blob-storage/views/new.html: -------------------------------------------------------------------------------- 1 | <%- include('__layout/header.html') %> 2 | 3 |

Create a new rental

4 | 5 |
6 |
7 | 8 | 17 |
18 |
19 | 20 | 29 |
30 |
31 | 32 | 40 |
41 |
42 | 43 |
44 |
45 | $ 46 |
47 | 55 |
56 |
57 | 58 |
59 | 60 |
61 | 73 |
74 |
75 |
76 | 77 |
78 | 89 |
90 |
91 |
92 | 93 |
94 | 105 |
106 |
107 |
108 | 109 |
110 | 118 |
119 |
120 | 121 | 122 |
123 | Cancel 124 | 125 |
126 |
127 | 128 | <%- include('__layout/footer.html') %> 129 | -------------------------------------------------------------------------------- /90-Playwright-simple-tests/.env.sample: -------------------------------------------------------------------------------- 1 | TEST_URL_BASE= -------------------------------------------------------------------------------- /90-Playwright-simple-tests/.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '14.x' 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Install Playwright 19 | run: npx playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: npx playwright test 22 | - uses: actions/upload-artifact@v2 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /90-Playwright-simple-tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | ./**/node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | test-results/ 110 | playwright-report/ 111 | -------------------------------------------------------------------------------- /90-Playwright-simple-tests/e2e-tests/example.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('https://demo.playwright.dev/todomvc'); 6 | }); 7 | 8 | const TODO_ITEMS = [ 9 | 'buy some cheese', 10 | 'feed the cat', 11 | 'book a doctors appointment' 12 | ]; 13 | 14 | test.describe('New Todo', () => { 15 | test('should allow me to add todo items', async ({ page }) => { 16 | // Create 1st todo. 17 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 18 | await page.locator('.new-todo').press('Enter'); 19 | 20 | // Make sure the list only has one todo item. 21 | await expect(page.locator('.view label')).toHaveText([ 22 | TODO_ITEMS[0] 23 | ]); 24 | 25 | // Create 2nd todo. 26 | await page.locator('.new-todo').fill(TODO_ITEMS[1]); 27 | await page.locator('.new-todo').press('Enter'); 28 | 29 | // Make sure the list now has two todo items. 30 | await expect(page.locator('.view label')).toHaveText([ 31 | TODO_ITEMS[0], 32 | TODO_ITEMS[1] 33 | ]); 34 | 35 | await checkNumberOfTodosInLocalStorage(page, 2); 36 | }); 37 | 38 | test('should clear text input field when an item is added', async ({ page }) => { 39 | // Create one todo item. 40 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 41 | await page.locator('.new-todo').press('Enter'); 42 | 43 | // Check that input is empty. 44 | await expect(page.locator('.new-todo')).toBeEmpty(); 45 | await checkNumberOfTodosInLocalStorage(page, 1); 46 | }); 47 | 48 | test('should append new items to the bottom of the list', async ({ page }) => { 49 | // Create 3 items. 50 | await createDefaultTodos(page); 51 | 52 | // Check test using different methods. 53 | await expect(page.locator('.todo-count')).toHaveText('3 items left'); 54 | await expect(page.locator('.todo-count')).toContainText('3'); 55 | await expect(page.locator('.todo-count')).toHaveText(/3/); 56 | 57 | // Check all items in one call. 58 | await expect(page.locator('.view label')).toHaveText(TODO_ITEMS); 59 | await checkNumberOfTodosInLocalStorage(page, 3); 60 | }); 61 | 62 | test('should show #main and #footer when items added', async ({ page }) => { 63 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 64 | await page.locator('.new-todo').press('Enter'); 65 | 66 | await expect(page.locator('.main')).toBeVisible(); 67 | await expect(page.locator('.footer')).toBeVisible(); 68 | await checkNumberOfTodosInLocalStorage(page, 1); 69 | }); 70 | }); 71 | 72 | test.describe('Mark all as completed', () => { 73 | test.beforeEach(async ({ page }) => { 74 | await createDefaultTodos(page); 75 | await checkNumberOfTodosInLocalStorage(page, 3); 76 | }); 77 | 78 | test.afterEach(async ({ page }) => { 79 | await checkNumberOfTodosInLocalStorage(page, 3); 80 | }); 81 | 82 | test('should allow me to mark all items as completed', async ({ page }) => { 83 | // Complete all todos. 84 | await page.locator('.toggle-all').check(); 85 | 86 | // Ensure all todos have 'completed' class. 87 | await expect(page.locator('.todo-list li')).toHaveClass(['completed', 'completed', 'completed']); 88 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 89 | }); 90 | 91 | test('should allow me to clear the complete state of all items', async ({ page }) => { 92 | // Check and then immediately uncheck. 93 | await page.locator('.toggle-all').check(); 94 | await page.locator('.toggle-all').uncheck(); 95 | 96 | // Should be no completed classes. 97 | await expect(page.locator('.todo-list li')).toHaveClass(['', '', '']); 98 | }); 99 | 100 | test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { 101 | const toggleAll = page.locator('.toggle-all'); 102 | await toggleAll.check(); 103 | await expect(toggleAll).toBeChecked(); 104 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 105 | 106 | // Uncheck first todo. 107 | const firstTodo = page.locator('.todo-list li').nth(0); 108 | await firstTodo.locator('.toggle').uncheck(); 109 | 110 | // Reuse toggleAll locator and make sure its not checked. 111 | await expect(toggleAll).not.toBeChecked(); 112 | 113 | await firstTodo.locator('.toggle').check(); 114 | await checkNumberOfCompletedTodosInLocalStorage(page, 3); 115 | 116 | // Assert the toggle all is checked again. 117 | await expect(toggleAll).toBeChecked(); 118 | }); 119 | }); 120 | 121 | test.describe('Item', () => { 122 | 123 | test('should allow me to mark items as complete', async ({ page }) => { 124 | // Create two items. 125 | for (const item of TODO_ITEMS.slice(0, 2)) { 126 | await page.locator('.new-todo').fill(item); 127 | await page.locator('.new-todo').press('Enter'); 128 | } 129 | 130 | // Check first item. 131 | const firstTodo = page.locator('.todo-list li').nth(0); 132 | await firstTodo.locator('.toggle').check(); 133 | await expect(firstTodo).toHaveClass('completed'); 134 | 135 | // Check second item. 136 | const secondTodo = page.locator('.todo-list li').nth(1); 137 | await expect(secondTodo).not.toHaveClass('completed'); 138 | await secondTodo.locator('.toggle').check(); 139 | 140 | // Assert completed class. 141 | await expect(firstTodo).toHaveClass('completed'); 142 | await expect(secondTodo).toHaveClass('completed'); 143 | }); 144 | 145 | test('should allow me to un-mark items as complete', async ({ page }) => { 146 | // Create two items. 147 | for (const item of TODO_ITEMS.slice(0, 2)) { 148 | await page.locator('.new-todo').fill(item); 149 | await page.locator('.new-todo').press('Enter'); 150 | } 151 | 152 | const firstTodo = page.locator('.todo-list li').nth(0); 153 | const secondTodo = page.locator('.todo-list li').nth(1); 154 | await firstTodo.locator('.toggle').check(); 155 | await expect(firstTodo).toHaveClass('completed'); 156 | await expect(secondTodo).not.toHaveClass('completed'); 157 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 158 | 159 | await firstTodo.locator('.toggle').uncheck(); 160 | await expect(firstTodo).not.toHaveClass('completed'); 161 | await expect(secondTodo).not.toHaveClass('completed'); 162 | await checkNumberOfCompletedTodosInLocalStorage(page, 0); 163 | }); 164 | 165 | test('should allow me to edit an item', async ({ page }) => { 166 | await createDefaultTodos(page); 167 | 168 | const todoItems = page.locator('.todo-list li'); 169 | const secondTodo = todoItems.nth(1); 170 | await secondTodo.dblclick(); 171 | await expect(secondTodo.locator('.edit')).toHaveValue(TODO_ITEMS[1]); 172 | await secondTodo.locator('.edit').fill('buy some sausages'); 173 | await secondTodo.locator('.edit').press('Enter'); 174 | 175 | // Explicitly assert the new text value. 176 | await expect(todoItems).toHaveText([ 177 | TODO_ITEMS[0], 178 | 'buy some sausages', 179 | TODO_ITEMS[2] 180 | ]); 181 | await checkTodosInLocalStorage(page, 'buy some sausages'); 182 | }); 183 | }); 184 | 185 | test.describe('Editing', () => { 186 | test.beforeEach(async ({ page }) => { 187 | await createDefaultTodos(page); 188 | await checkNumberOfTodosInLocalStorage(page, 3); 189 | }); 190 | 191 | test('should hide other controls when editing', async ({ page }) => { 192 | const todoItem = page.locator('.todo-list li').nth(1); 193 | await todoItem.dblclick(); 194 | await expect(todoItem.locator('.toggle')).not.toBeVisible(); 195 | await expect(todoItem.locator('label')).not.toBeVisible(); 196 | await checkNumberOfTodosInLocalStorage(page, 3); 197 | }); 198 | 199 | test('should save edits on blur', async ({ page }) => { 200 | const todoItems = page.locator('.todo-list li'); 201 | await todoItems.nth(1).dblclick(); 202 | await todoItems.nth(1).locator('.edit').fill('buy some sausages'); 203 | await todoItems.nth(1).locator('.edit').dispatchEvent('blur'); 204 | 205 | await expect(todoItems).toHaveText([ 206 | TODO_ITEMS[0], 207 | 'buy some sausages', 208 | TODO_ITEMS[2], 209 | ]); 210 | await checkTodosInLocalStorage(page, 'buy some sausages'); 211 | }); 212 | 213 | test('should trim entered text', async ({ page }) => { 214 | const todoItems = page.locator('.todo-list li'); 215 | await todoItems.nth(1).dblclick(); 216 | await todoItems.nth(1).locator('.edit').fill(' buy some sausages '); 217 | await todoItems.nth(1).locator('.edit').press('Enter'); 218 | 219 | await expect(todoItems).toHaveText([ 220 | TODO_ITEMS[0], 221 | 'buy some sausages', 222 | TODO_ITEMS[2], 223 | ]); 224 | await checkTodosInLocalStorage(page, 'buy some sausages'); 225 | }); 226 | 227 | test('should remove the item if an empty text string was entered', async ({ page }) => { 228 | const todoItems = page.locator('.todo-list li'); 229 | await todoItems.nth(1).dblclick(); 230 | await todoItems.nth(1).locator('.edit').fill(''); 231 | await todoItems.nth(1).locator('.edit').press('Enter'); 232 | 233 | await expect(todoItems).toHaveText([ 234 | TODO_ITEMS[0], 235 | TODO_ITEMS[2], 236 | ]); 237 | }); 238 | 239 | test('should cancel edits on escape', async ({ page }) => { 240 | const todoItems = page.locator('.todo-list li'); 241 | await todoItems.nth(1).dblclick(); 242 | await todoItems.nth(1).locator('.edit').press('Escape'); 243 | await expect(todoItems).toHaveText(TODO_ITEMS); 244 | }); 245 | }); 246 | 247 | test.describe('Counter', () => { 248 | test('should display the current number of todo items', async ({ page }) => { 249 | await page.locator('.new-todo').fill(TODO_ITEMS[0]); 250 | await page.locator('.new-todo').press('Enter'); 251 | await expect(page.locator('.todo-count')).toContainText('1'); 252 | 253 | await page.locator('.new-todo').fill(TODO_ITEMS[1]); 254 | await page.locator('.new-todo').press('Enter'); 255 | await expect(page.locator('.todo-count')).toContainText('2'); 256 | 257 | await checkNumberOfTodosInLocalStorage(page, 2); 258 | }); 259 | }); 260 | 261 | test.describe('Clear completed button', () => { 262 | test.beforeEach(async ({ page }) => { 263 | await createDefaultTodos(page); 264 | }); 265 | 266 | test('should display the correct text', async ({ page }) => { 267 | await page.locator('.todo-list li .toggle').first().check(); 268 | await expect(page.locator('.clear-completed')).toHaveText('Clear completed'); 269 | }); 270 | 271 | test('should remove completed items when clicked', async ({ page }) => { 272 | const todoItems = page.locator('.todo-list li'); 273 | await todoItems.nth(1).locator('.toggle').check(); 274 | await page.locator('.clear-completed').click(); 275 | await expect(todoItems).toHaveCount(2); 276 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 277 | }); 278 | 279 | test('should be hidden when there are no items that are completed', async ({ page }) => { 280 | await page.locator('.todo-list li .toggle').first().check(); 281 | await page.locator('.clear-completed').click(); 282 | await expect(page.locator('.clear-completed')).toBeHidden(); 283 | }); 284 | }); 285 | 286 | test.describe('Persistence', () => { 287 | test('should persist its data', async ({ page }) => { 288 | for (const item of TODO_ITEMS.slice(0, 2)) { 289 | await page.locator('.new-todo').fill(item); 290 | await page.locator('.new-todo').press('Enter'); 291 | } 292 | 293 | const todoItems = page.locator('.todo-list li'); 294 | await todoItems.nth(0).locator('.toggle').check(); 295 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 296 | await expect(todoItems).toHaveClass(['completed', '']); 297 | 298 | // Ensure there is 1 completed item. 299 | checkNumberOfCompletedTodosInLocalStorage(page, 1); 300 | 301 | // Now reload. 302 | await page.reload(); 303 | await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); 304 | await expect(todoItems).toHaveClass(['completed', '']); 305 | }); 306 | }); 307 | 308 | test.describe('Routing', () => { 309 | test.beforeEach(async ({ page }) => { 310 | await createDefaultTodos(page); 311 | // make sure the app had a chance to save updated todos in storage 312 | // before navigating to a new view, otherwise the items can get lost :( 313 | // in some frameworks like Durandal 314 | await checkTodosInLocalStorage(page, TODO_ITEMS[0]); 315 | }); 316 | 317 | test('should allow me to display active items', async ({ page }) => { 318 | await page.locator('.todo-list li .toggle').nth(1).check(); 319 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 320 | await page.locator('.filters >> text=Active').click(); 321 | await expect(page.locator('.todo-list li')).toHaveCount(2); 322 | await expect(page.locator('.todo-list li')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); 323 | }); 324 | 325 | test('should respect the back button', async ({ page }) => { 326 | await page.locator('.todo-list li .toggle').nth(1).check(); 327 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 328 | 329 | await test.step('Showing all items', async () => { 330 | await page.locator('.filters >> text=All').click(); 331 | await expect(page.locator('.todo-list li')).toHaveCount(3); 332 | }); 333 | 334 | await test.step('Showing active items', async () => { 335 | await page.locator('.filters >> text=Active').click(); 336 | }); 337 | 338 | await test.step('Showing completed items', async () => { 339 | await page.locator('.filters >> text=Completed').click(); 340 | }); 341 | 342 | await expect(page.locator('.todo-list li')).toHaveCount(1); 343 | await page.goBack(); 344 | await expect(page.locator('.todo-list li')).toHaveCount(2); 345 | await page.goBack(); 346 | await expect(page.locator('.todo-list li')).toHaveCount(3); 347 | }); 348 | 349 | test('should allow me to display completed items', async ({ page }) => { 350 | await page.locator('.todo-list li .toggle').nth(1).check(); 351 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 352 | await page.locator('.filters >> text=Completed').click(); 353 | await expect(page.locator('.todo-list li')).toHaveCount(1); 354 | }); 355 | 356 | test('should allow me to display all items', async ({ page }) => { 357 | await page.locator('.todo-list li .toggle').nth(1).check(); 358 | await checkNumberOfCompletedTodosInLocalStorage(page, 1); 359 | await page.locator('.filters >> text=Active').click(); 360 | await page.locator('.filters >> text=Completed').click(); 361 | await page.locator('.filters >> text=All').click(); 362 | await expect(page.locator('.todo-list li')).toHaveCount(3); 363 | }); 364 | 365 | test('should highlight the currently applied filter', async ({ page }) => { 366 | await expect(page.locator('.filters >> text=All')).toHaveClass('selected'); 367 | await page.locator('.filters >> text=Active').click(); 368 | // Page change - active items. 369 | await expect(page.locator('.filters >> text=Active')).toHaveClass('selected'); 370 | await page.locator('.filters >> text=Completed').click(); 371 | // Page change - completed items. 372 | await expect(page.locator('.filters >> text=Completed')).toHaveClass('selected'); 373 | }); 374 | }); 375 | 376 | async function createDefaultTodos(page) { 377 | for (const item of TODO_ITEMS) { 378 | await page.locator('.new-todo').fill(item); 379 | await page.locator('.new-todo').press('Enter'); 380 | } 381 | } 382 | 383 | /** 384 | * @param {import('@playwright/test').Page} page 385 | * @param {number} expected 386 | */ 387 | async function checkNumberOfTodosInLocalStorage(page, expected) { 388 | return await page.waitForFunction(e => { 389 | return JSON.parse(localStorage['react-todos']).length === e; 390 | }, expected); 391 | } 392 | 393 | /** 394 | * @param {import('@playwright/test').Page} page 395 | * @param {number} expected 396 | */ 397 | async function checkNumberOfCompletedTodosInLocalStorage(page, expected) { 398 | return await page.waitForFunction(e => { 399 | return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e; 400 | }, expected); 401 | } 402 | 403 | /** 404 | * @param {import('@playwright/test').Page} page 405 | * @param {string} title 406 | */ 407 | async function checkTodosInLocalStorage(page, title) { 408 | return await page.waitForFunction(t => { 409 | return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t); 410 | }, title); 411 | } 412 | -------------------------------------------------------------------------------- /90-Playwright-simple-tests/e2e-tests/rental.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { test, expect } = require('@playwright/test'); 3 | 4 | // page is fixture coming in as context 5 | // codegen gives dom elements 6 | test('basic test', async ({ page }) => { 7 | console.log(`URL = ${process.env.TEST_URL_BASE}`) 8 | await page.goto(`${process.env.TEST_URL_BASE}/`); 9 | const title = page.locator('.navbar__inner .navbar__title'); 10 | await expect(title).toHaveText('Playwright'); 11 | }); -------------------------------------------------------------------------------- /90-Playwright-simple-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "90-playwright-simple-tests", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "playwright test" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "devDependencies": { 11 | "@playwright/test": "^1.20.0" 12 | }, 13 | "dependencies": { 14 | "dotenv": "^16.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /90-Playwright-simple-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { devices } = require('@playwright/test'); 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | require('dotenv').config(); 9 | 10 | 11 | /** 12 | * @see https://playwright.dev/docs/test-configuration 13 | * @type {import('@playwright/test').PlaywrightTestConfig} 14 | */ 15 | const config = { 16 | testDir: './e2e-tests', 17 | /* Maximum time one test can run for. */ 18 | timeout: 30 * 1000, 19 | expect: { 20 | /** 21 | * Maximum time expect() should wait for the condition to be met. 22 | * For example in `await expect(locator).toHaveText();` 23 | */ 24 | timeout: 5000 25 | }, 26 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 27 | forbidOnly: !!process.env.CI, 28 | /* Retry on CI only */ 29 | retries: process.env.CI ? 2 : 0, 30 | /* Opt out of parallel tests on CI. */ 31 | workers: process.env.CI ? 1 : undefined, 32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 33 | reporter: 'html', 34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 35 | use: { 36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 37 | actionTimeout: 0, 38 | /* Base URL to use in actions like `await page.goto('/')`. */ 39 | // baseURL: 'http://localhost:3000', 40 | 41 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 42 | trace: 'on', 43 | }, 44 | 45 | /* Configure projects for major browsers */ 46 | projects: [ 47 | { 48 | name: 'chromium', 49 | use: { 50 | ...devices['Desktop Chrome'], 51 | }, 52 | }, 53 | 54 | { 55 | name: 'firefox', 56 | use: { 57 | ...devices['Desktop Firefox'], 58 | }, 59 | }, 60 | 61 | { 62 | name: 'webkit', 63 | use: { 64 | ...devices['Desktop Safari'], 65 | }, 66 | }, 67 | 68 | /* Test against mobile viewports. */ 69 | { 70 | name: 'Mobile Chrome', 71 | use: { 72 | ...devices['Pixel 5'], 73 | }, 74 | }, 75 | { 76 | name: 'Mobile Safari', 77 | use: { 78 | ...devices['iPhone 12'], 79 | }, 80 | }, 81 | 82 | /* Test against branded browsers. */ 83 | // { 84 | // name: 'Microsoft Edge', 85 | // use: { 86 | // channel: 'msedge', 87 | // }, 88 | // }, 89 | // { 90 | // name: 'Google Chrome', 91 | // use: { 92 | // channel: 'chrome', 93 | // }, 94 | // }, 95 | ], 96 | 97 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 98 | // outputDir: 'test-results/', 99 | 100 | /* Run your local dev server before starting the tests */ 101 | // webServer: { 102 | // command: 'npm run start', 103 | // port: 3000, 104 | // }, 105 | }; 106 | 107 | module.exports = config; 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript on Azure Learn Path - Module 2 - Deploying a basic app to Azure 2 | 3 | This Learn module requires the following Azure resources to deploy correctly: 4 | 5 | * Azure App Service 6 | * Azure Cosmos DB with MongoDB API 7 | 8 | ## Requirements 9 | 10 | - Node.js LTS 11 | 12 | ## Local development 13 | 14 | - Create Azure resources 15 | - Azure App Service + Cosmos DB for MongoDB API 16 | - [Create resource](https://ms.portal.azure.com/#create/Microsoft.AppServiceWebAppDatabaseV3) in Azure portal 17 | - Create database 18 | - Create collection 19 | - Copy the following to the `.env` file: 20 | - Connection string 21 | - Database name 22 | - Collection name 23 | - Azure Storage (for images) 24 | - [Create resource](https://ms.portal.azure.com/#create/Microsoft.StorageAccount) 25 | - Make sure `Blob public access` is enabled 26 | - Create container 27 | - Copy the following to the `.env` file: 28 | - Connection string 29 | - Container name 30 | - Install npm dependencies: `npm install` 31 | - Verify environment variables are set in `.env` 32 | - PORT=8080 - default port for Azure App Service 33 | - MONGODB_URI= 34 | - MONGODB_URI_DATABASE_NAME= 35 | - MONGODB_URI_COLLECTION_NAME= 36 | - AZURE_STORAGE_BLOB_CONNECTIONSTRING= 37 | - AZURE_STORAGE_BLOB_CONTAINER_NAME= 38 | - Start the server: `npm start` 39 | - Access Web App at: `http://127.0.0.1:8080` 40 | 41 | ## Azure portal: Configure git to push to Azure App Service 42 | 43 | 1. In the Azure portal, for your web app, select **Deployment -> Deployment Center**. 44 | 1. On the **Settings** tab, copy the **Git Clone URI**. This will become your local git value for your remote named `Azure`. 45 | 1. On the **Local Git/FTPS credentials** tab, copy the **Username** and **Password** under the application scope. These credentials allow you to deploy _only_ to this web app. 46 | 47 | ## Azure CLI: Configure git to push to Azure App Service 48 | 49 | 1. Create a user scope credential for the web app. 50 | 51 | ```azurecli 52 | az webapp deployment user set --user-name --password 53 | ``` 54 | 55 | 1. After app is created, configure deployment from local git 56 | 57 | ```azurecli 58 | az webapp deployment source config-local-git --name --resource-group 59 | ``` 60 | 61 | The output contains a URL like: https://@.scm.azurewebsites.net/.git. Use this URL to deploy your app in the next step. 62 | 63 | ## Create local git remote to Azure App Service 64 | 65 | 1. In a local terminal window, change the directory to the root of your Git repository, and add a Git remote using the URL you got from your app. If your chosen method doesn't give you a URL, use https://.scm.azurewebsites.net/.git with your app name in . 66 | 67 | ```bash 68 | git remote add azure 69 | ``` 70 | 71 | 1. Push to the Azure remote with: 72 | 73 | ```bash 74 | git push azure 75 | ``` 76 | 77 | 1. In the Git Credential Manager window, enter your user-scope or application-scope credentials, not your Azure sign-in credentials. 78 | 79 | If your Git remote URL already contains the username and password, you won't be prompted. 80 | 81 | 1. Review the output. You may see runtime-specific automation. 82 | 83 | 1. Browse to your cloud app to verify that the content is deployed: 84 | 85 | ```http 86 | http://.azurewebsites.net 87 | ``` -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | ## JavaScript on Azure Learn Path 2 | 3 | * Module 1 - Getting Started with JS on Azure 4 | * Module 2 - Deploy basic app to Azure App Service 5 | * Module 3 - Using CosmosDB to store MongoDB data from JS Applications 6 | * Module 4 - Securing access to Azure Services 7 | * Module 5 - Using Azure Blob Storage from JS Applications 8 | * Module 6 - Authenticating and Authorizing users using Azure Active Directory in JS Applications 9 | 10 | ## Module 2 11 | 12 | Deploy basic app to Azure App Service 13 | 14 | * Create a new Azure App Service 15 | * Deploy with VSCode App Service extension 16 | * Deployed app will show data from `.json` data file. 17 | 18 | ## Module 3 19 | 20 | Using CosmosDB to store MongoDB data from JS Applications 21 | 22 | * Create Azure Cosmos DB with MongoDB API resource 23 | * Create `.env` file with connection string and database name 24 | * Deployed app shows listings without pictures 25 | 26 | ## Module 4 27 | 28 | Securing access to Azure Services: move MongoDB secrets into Key Vault because they aren’t currently supported through managed identity. 29 | 30 | * Deployed app editing requires auth 31 | 32 | 33 | ## Module 5 34 | 35 | Using Azure Blob Storage from JS Applications: App Service + MI. This is an integration after the DB is integrated. Images are secondary. 36 | 37 | * Deployed app shows listings with pictures 38 | 39 | ## Module 6 40 | 41 | --------------------------------------------------------------------------------