├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── functions ├── .funcignore ├── .gitignore ├── heroes-delete │ ├── function.json │ └── index.ts ├── heroes-get │ ├── function.json │ └── index.ts ├── heroes-post │ ├── function.json │ └── index.ts ├── heroes-put │ ├── function.json │ └── index.ts ├── host.json ├── local.settings.example.json ├── package-lock.json ├── package.json ├── proxies.json ├── services │ ├── config.ts │ ├── hero.service.ts │ ├── index.ts │ └── villain.service.ts ├── tsconfig.json ├── villains-delete │ ├── function.json │ └── index.ts ├── villains-get │ ├── function.json │ └── index.ts ├── villains-post │ ├── function.json │ └── index.ts └── villains-put │ ├── function.json │ └── index.ts ├── package-lock.json ├── package.json ├── server ├── index.ts ├── routes │ ├── hero.routes.ts │ ├── index.ts │ └── villain.routes.ts ├── server.ts ├── services │ ├── config.ts │ ├── hero.service.ts │ ├── index.ts │ └── villain.service.ts └── tsconfig.json ├── src ├── app │ ├── about.component.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.ts │ │ ├── header-bar-brand.component.ts │ │ ├── header-bar-links.component.ts │ │ ├── header-bar.component.ts │ │ ├── index.ts │ │ ├── model │ │ │ ├── hero.ts │ │ │ ├── index.ts │ │ │ └── villain.ts │ │ ├── nav.component.ts │ │ └── not-found.component.ts │ ├── heroes │ │ ├── hero-detail │ │ │ ├── hero-detail.component.html │ │ │ └── hero-detail.component.ts │ │ ├── hero-list │ │ │ ├── hero-list.component.html │ │ │ └── hero-list.component.ts │ │ ├── hero.service.ts │ │ ├── heroes.module.ts │ │ └── heroes │ │ │ ├── heroes.component.html │ │ │ └── heroes.component.ts │ ├── shared │ │ ├── button-footer.component.ts │ │ ├── card-content.component.ts │ │ ├── list-header.component.ts │ │ ├── modal.component.ts │ │ └── shared.module.ts │ ├── store │ │ ├── config.ts │ │ ├── entity-metadata.ts │ │ └── store.module.ts │ └── villains │ │ ├── villain-detail │ │ ├── villain-detail.component.html │ │ └── villain-detail.component.ts │ │ ├── villain-list │ │ ├── villain-list.component.html │ │ └── villain-list.component.ts │ │ ├── villain.service.ts │ │ ├── villains.module.ts │ │ └── villains │ │ ├── villains.component.html │ │ └── villains.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── proxy.conf.json ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=7070 3 | WWW=./ 4 | 5 | CORE_API_KEY=your-core-api-key-goes-here 6 | CORE_API_URL=https://your-azure-cosmos-db-url-goes-here:443/ 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # /node_modules/* in the project root is ignored by default 2 | # build artefacts 3 | dist/* 4 | coverage/* 5 | # data definition files 6 | **/*.d.ts 7 | # 3rd party libs 8 | /src/public/ 9 | # custom definition files 10 | /src/types/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "rules": { 9 | "semi": ["error", "always"], 10 | "quotes": ["error", "single"], 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-unused-vars": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.bak 3 | 4 | # data files 5 | /data 6 | 7 | # compiled output 8 | dist 9 | server/dist 10 | 11 | /tmp 12 | /out-tsc 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # System Files 18 | .DS_Store 19 | Thumbs.db 20 | 21 | bin 22 | obj 23 | csx 24 | .vs 25 | edge 26 | Publish 27 | 28 | *.user 29 | *.suo 30 | *.cscfg 31 | *.Cache 32 | project.lock.json 33 | 34 | /packages 35 | /TestResults 36 | 37 | /tools/NuGet.exe 38 | /App_Data 39 | /secrets 40 | /data 41 | .secrets 42 | appsettings.json 43 | local.settings.json 44 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Angular", 6 | "type": "chrome", 7 | "request": "launch", 8 | "preLaunchTask": "npm: start", 9 | "url": "http://localhost:4200/", 10 | "webRoot": "${workspaceFolder}" 11 | }, 12 | { 13 | "type": "node", 14 | "request": "launch", 15 | "name": "Launch Node Express via NPM", 16 | "runtimeExecutable": "npm", 17 | "runtimeArgs": ["run-script", "node:debug"], 18 | "port": 9229 19 | }, 20 | { 21 | "name": "Attach to Node Functions", 22 | "type": "node", 23 | "request": "attach", 24 | "port": 9229, 25 | "preLaunchTask": "func: host start" 26 | } 27 | ], 28 | "compounds": [ 29 | { 30 | "name": "Debug Functions and Angular", 31 | "configurations": ["Attach to Node Functions", "Launch Angular"] 32 | }, 33 | { 34 | "name": "Debug Express and Angular", 35 | "configurations": ["Launch Node Express via NPM", "Launch Angular"] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#2f7c47", 4 | "activityBar.activeBorder": "#422c74", 5 | "activityBar.foreground": "#e7e7e7", 6 | "activityBar.inactiveForeground": "#e7e7e799", 7 | "activityBarBadge.background": "#422c74", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "titleBar.activeBackground": "#215732", 10 | "titleBar.inactiveBackground": "#21573299", 11 | "titleBar.activeForeground": "#e7e7e7", 12 | "titleBar.inactiveForeground": "#e7e7e799", 13 | "statusBar.background": "#215732", 14 | "statusBarItem.hoverBackground": "#2f7c47", 15 | "statusBar.foreground": "#e7e7e7", 16 | "panel.border": "#2f7c47", 17 | "sideBar.border": "#2f7c47", 18 | "editorGroup.border": "#2f7c47" 19 | }, 20 | "peacock.color": "#215732", 21 | "azureFunctions.deploySubpath": "functions", 22 | "azureFunctions.projectLanguage": "TypeScript", 23 | "azureFunctions.projectRuntime": "~3", 24 | "debug.internalConsoleOptions": "neverOpen", 25 | "azureFunctions.preDeployTask": "npm prune" 26 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "isBackground": true, 8 | "presentation": { 9 | "focus": true, 10 | "panel": "dedicated" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "problemMatcher": { 17 | "owner": "typescript", 18 | "source": "ts", 19 | "applyTo": "closedDocuments", 20 | "fileLocation": [ 21 | "relative", 22 | "${cwd}" 23 | ], 24 | "pattern": "$tsc", 25 | "background": { 26 | "activeOnStart": true, 27 | "beginsPattern": { 28 | "regexp": "(.*?)" 29 | }, 30 | "endsPattern": { 31 | "regexp": "Compiled |Failed to compile." 32 | } 33 | } 34 | } 35 | }, 36 | { 37 | "type": "func", 38 | "command": "host start", 39 | "problemMatcher": "$func-watch", 40 | "isBackground": true, 41 | "dependsOn": "npm build", 42 | "options": { 43 | "cwd": "${workspaceFolder}/functions" 44 | } 45 | }, 46 | { 47 | "type": "shell", 48 | "label": "npm build", 49 | "command": "npm run build", 50 | "dependsOn": "npm install", 51 | "problemMatcher": "$tsc", 52 | "options": { 53 | "cwd": "${workspaceFolder}/functions" 54 | } 55 | }, 56 | { 57 | "type": "shell", 58 | "label": "npm install", 59 | "command": "npm install", 60 | "options": { 61 | "cwd": "${workspaceFolder}/functions" 62 | } 63 | }, 64 | { 65 | "type": "shell", 66 | "label": "npm prune", 67 | "command": "npm prune --production", 68 | "dependsOn": "npm build", 69 | "problemMatcher": [], 70 | "options": { 71 | "cwd": "${workspaceFolder}/functions" 72 | } 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heroes Angular Serverless 2 | 3 | TypeScript Node/Express 👉TypeScript Serverless ➕ Angular 4 | 5 | This project was created to help represent a fundamental app written with Node Express APIs and TypeScript that can be shifted to Serverless Functions with TypeScript. 6 | 7 | The client app is Angular, however it could just as easily be Vue or React. The heroes and villains theme is used throughout the app. 8 | 9 | by [John Papa](http://twitter.com/john_papa) 10 | 11 | Comparative client apps written with Vue and React can be found at at [github.com/johnpapa/heroes-vue](https://github.com/johnpapa/heroes-vue) and [github.com/johnpapa/heroes-react](https://github.com/johnpapa/heroes-react) 12 | 13 | ## Why 14 | 15 | I love Node and Express for creating APIs! These require a server and paying for that server in the cloud. Shifting to serverless alleviates the cost, the server upkeep, helps scale up and down easily, and reduces the surface area of the middleware required for a robust Express app. Is it perfect? No, of course not! But this is a solid option if these factors affect you. 16 | 17 | ## Prerequisites 18 | 19 | 1. We need a database. You can use the free [Azure Cosmos DB](https://azure.microsoft.com/en-us/try/cosmosdb/?wt.mc_id=heroesangularserverless-github-jopapa) trial. Or try the [Azure Free Trial](https://azure.microsoft.com/en-us/free/?wt.mc_id=heroesangularserverless-github-jopapa). 20 | 1. Create an Azure Cosmos DB database. 21 | 1. Create a container named `heroes` and another named `villains`. Give each a partition key of `/id`. You can add data through the app later. 22 | 1. Make a copy of the `env.example` file named `.env`, in the root of the project. It should contain the following code. Replace the `CORE_API_KEY` and `CORE_API_URL` variables with your Azure Cosmos DB information. 23 | 24 | `.env` 25 | 26 | ``` 27 | NODE_ENV=development 28 | PORT=7070 29 | WWW="./" 30 | CORE_API_KEY="your-azure-cosmos-db-api-key-goes-here" 31 | CORE_API_URL="https://papa-cosmos-api-db.documents.azure.com/?WT.mc_id=heroesangularserverless-github-jopapa" 32 | ``` 33 | 34 | Open the `/functions/local.settings.json` file and add the `CORE_API_KEY` and `CORE_API_URL` variables as shown below. Fill those values in with your database information. 35 | 36 | `/functions/local.settings.json` 37 | 38 | ```json 39 | { 40 | "IsEncrypted": false, 41 | "Values": { 42 | "AzureWebJobsStorage": "", 43 | "FUNCTIONS_WORKER_RUNTIME": "node", 44 | "CORE_API_KEY": "your-azure-cosmos-db-api-key-goes-here", 45 | "CORE_API_URL": "https://your-azure-cosmos-db-url-goes-here:443/" 46 | } 47 | } 48 | ``` 49 | 50 | ## Getting Started 51 | 52 | 1. Clone this repository 53 | 54 | ```bash 55 | git clone https://github.com/johnpapa/heroes-angular-serverless.git heroes 56 | cd heroes 57 | ``` 58 | 59 | 1. Install the npm packages 60 | 61 | ```bash 62 | npm install 63 | ``` 64 | 65 | 1. Build the Node Express and the Angular code 66 | 67 | ```bash 68 | npm run node-ng:build 69 | ``` 70 | 71 | 1. Run the app 72 | 73 | ```bash 74 | npm run node:start 75 | ``` 76 | 77 | ## Debug Node Express and Angular 78 | 79 | 1. Open `proxy.conf.json` and change the port to `7070` 80 | 81 | 1. Open the VS Code Command Palette `F1` 82 | 83 | 1. Type `View: Show Debug` and press `ENTER` 84 | 85 | 1. Select `Debug Express and Angular` 86 | 87 | 1. Press `F5` 88 | 89 | 1. Open the browser to 90 | 91 | You may now set break points in the Express and Angular code. 92 | 93 | ## Debug Functions and Angular 94 | 95 | 1. Open `proxy.conf.json` and change the port to `7071` 96 | 97 | 1. Open the VS Code Command Palette `F1` 98 | 99 | 1. Type `View: Show Debug` and press `ENTER` 100 | 101 | 1. Select `Debug Functions and Angular` 102 | 103 | 1. Press `F5` 104 | 105 | 1. Open the browser to 106 | 107 | You may now set break points in the Functions and Angular code. 108 | 109 | ## Problems or Suggestions 110 | 111 | [Open an issue here](/issues) 112 | 113 | ## Resources 114 | 115 | - [Azure Free Trial](https://azure.microsoft.com/en-us/free/?wt.mc_id=heroesangularserverless-github-jopapa) 116 | - [VS Code](https://code.visualstudio.com?wt.mc_id=heroesangularserverless-github-jopapa) 117 | - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=heroesangularserverless-github-jopapa) 118 | - [VS Code Extension Marketplace](https://marketplace.visualstudio.com/vscode?wt.mc_id=heroesangularserverless-github-jopapa) 119 | - [VS Code - macOS keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf?WT.mc_id=heroesangularserverless-github-jopapa) 120 | - [VS Code - Windows keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf?WT.mc_id=heroesangularserverless-github-jopapa) 121 | - Azure Functions [local.settings.json](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local#local-settings-file?WT.mc_id=heroesangularserverless-github-jopapa) file 122 | - Tutorial to [Deploy to Azure Using Azure Functions](https://code.visualstudio.com/tutorials/functions-extension/getting-started?WT.mc_id=heroesangularserverless-github-jopapa) 123 | 124 | ### Debugging Resources 125 | 126 | - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?wt.mc_id=heroesangularserverless-github-jopapa) 127 | - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?wt.mc_id=heroesangularserverless-github-jopapa) 128 | - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?wt.mc_id=heroesangularserverless-github-jopapa) 129 | - [Tasks in VS Code](https://code.visualstudio.com/Docs/editor/tasks?wt.mc_id=heroesangularserverless-github-jopapa) 130 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "heroes-angular": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "aot": true, 21 | "outputPath": "dist/heroes-angular", 22 | "index": "src/index.html", 23 | "main": "src/main.ts", 24 | "polyfills": "src/polyfills.ts", 25 | "tsConfig": "src/tsconfig.app.json", 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "stylePreprocessorOptions": { 28 | "includePaths": [ 29 | "src/styles" 30 | ] 31 | }, 32 | "styles": [ 33 | "node_modules/bulma/css/bulma.min.css", 34 | "node_modules/bulma/bulma.sass", 35 | "node_modules/font-awesome/css/font-awesome.css", 36 | "src/styles.scss" 37 | ], 38 | "scripts": [] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "extractCss": true, 52 | "namedChunks": false, 53 | "aot": true, 54 | "extractLicenses": true, 55 | "vendorChunk": false, 56 | "buildOptimizer": true, 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "2mb", 61 | "maximumError": "5mb" 62 | }, 63 | { 64 | "type": "anyComponentStyle", 65 | "maximumWarning": "6kb" 66 | } 67 | ] 68 | } 69 | } 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "options": { 74 | "browserTarget": "heroes-angular:build" 75 | }, 76 | "configurations": { 77 | "production": { 78 | "browserTarget": "heroes-angular:build:production" 79 | } 80 | } 81 | }, 82 | "extract-i18n": { 83 | "builder": "@angular-devkit/build-angular:extract-i18n", 84 | "options": { 85 | "browserTarget": "heroes-angular:build" 86 | } 87 | }, 88 | "test": { 89 | "builder": "@angular-devkit/build-angular:karma", 90 | "options": { 91 | "main": "src/test.ts", 92 | "polyfills": "src/polyfills.ts", 93 | "tsConfig": "src/tsconfig.spec.json", 94 | "karmaConfig": "src/karma.conf.js", 95 | "styles": ["src/styles.scss"], 96 | "scripts": [], 97 | "assets": ["src/favicon.ico", "src/assets"] 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-devkit/build-angular:tslint", 102 | "options": { 103 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 104 | "exclude": ["**/node_modules/**"] 105 | } 106 | } 107 | } 108 | }, 109 | "heroes-angular-e2e": { 110 | "root": "e2e/", 111 | "projectType": "application", 112 | "prefix": "", 113 | "architect": { 114 | "e2e": { 115 | "builder": "@angular-devkit/build-angular:protractor", 116 | "options": { 117 | "protractorConfig": "e2e/protractor.conf.js", 118 | "devServerTarget": "heroes-angular:serve" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "devServerTarget": "heroes-angular:serve:production" 123 | } 124 | } 125 | }, 126 | "lint": { 127 | "builder": "@angular-devkit/build-angular:tslint", 128 | "options": { 129 | "tsConfig": "e2e/tsconfig.e2e.json", 130 | "exclude": ["**/node_modules/**"] 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "defaultProject": "heroes-angular" 137 | } 138 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to heroes-angular!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /functions/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Azure Functions artifacts 83 | bin 84 | obj 85 | appsettings.json 86 | local.settings.json 87 | 88 | # TypeScript output 89 | dist 90 | out 91 | -------------------------------------------------------------------------------- /functions/heroes-delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["delete"], 10 | "route": "heroes/{id}" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/heroes-delete/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/heroes-delete/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { heroService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await heroService.deleteHero(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/heroes-get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["get"], 10 | "route": "heroes" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/heroes-get/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/heroes-get/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { heroService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await heroService.getHeroes(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/heroes-post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["post"], 10 | "route": "heroes" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/heroes-post/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/heroes-post/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { heroService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await heroService.postHero(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/heroes-put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["put"], 10 | "route": "heroes/{id}" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/heroes-put/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/heroes-put/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { heroService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await heroService.putHero(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } 4 | -------------------------------------------------------------------------------- /functions/local.settings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node", 6 | "githubkey": "go here to generate a token https://github.com/settings/tokens", 7 | "CORE_API_KEY": "cosmos key here", 8 | "CORE_API_URL": "cosmos url here" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /functions/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@azure/cosmos": { 8 | "version": "3.2.0", 9 | "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.2.0.tgz", 10 | "integrity": "sha512-ULgHquAr6LuMs8nQR+6ra9Hv3Psm6Ckku1XN3+9IqG+VsUZ7wgFfBLMnDGJMfm2MA50yRmajFmjuu2EeqRNWCQ==", 11 | "requires": { 12 | "@azure/cosmos-sign": "1.0.2", 13 | "@types/debug": "^4.1.4", 14 | "atob": "2.1.2", 15 | "binary-search-bounds": "2.0.3", 16 | "crypto-hash": "1.1.0", 17 | "debug": "^4.1.1", 18 | "fast-json-stable-stringify": "2.0.0", 19 | "node-abort-controller": "1.0.3", 20 | "node-fetch": "2.6.0", 21 | "priorityqueuejs": "1.0.0", 22 | "semaphore": "1.0.5", 23 | "tslib": "^1.9.3", 24 | "universal-user-agent": "2.1.0", 25 | "uuid": "3.3.2" 26 | } 27 | }, 28 | "@azure/cosmos-sign": { 29 | "version": "1.0.2", 30 | "resolved": "https://registry.npmjs.org/@azure/cosmos-sign/-/cosmos-sign-1.0.2.tgz", 31 | "integrity": "sha512-+y3EDcbSFuieOKqw9VyaX7D13LB4LycQIdmLwSqFnSUO0mWl+RBLCKW3RL6XiyWOHRV2sMNT9vwGzwiFZI70vQ==", 32 | "requires": { 33 | "crypto-js": "3.1.9-1" 34 | } 35 | }, 36 | "@azure/functions": { 37 | "version": "1.0.1-beta2", 38 | "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-1.0.1-beta2.tgz", 39 | "integrity": "sha512-ewVNxU2fqSCLbLuHwwvcL2ExgYNIhaztgHQfBShM9bpCBlAufTrvqlGnsEMfYv2F+BmJrkvhcDWE7E8cDz4X0g==", 40 | "dev": true 41 | }, 42 | "@types/debug": { 43 | "version": "4.1.5", 44 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", 45 | "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" 46 | }, 47 | "atob": { 48 | "version": "2.1.2", 49 | "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", 50 | "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" 51 | }, 52 | "binary-search-bounds": { 53 | "version": "2.0.3", 54 | "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz", 55 | "integrity": "sha1-X/hhbW3SylOIvIWy1iZuK52lAtw=" 56 | }, 57 | "cross-spawn": { 58 | "version": "6.0.5", 59 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 60 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 61 | "requires": { 62 | "nice-try": "^1.0.4", 63 | "path-key": "^2.0.1", 64 | "semver": "^5.5.0", 65 | "shebang-command": "^1.2.0", 66 | "which": "^1.2.9" 67 | } 68 | }, 69 | "crypto-hash": { 70 | "version": "1.1.0", 71 | "resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-1.1.0.tgz", 72 | "integrity": "sha512-5DWmfCxQZHWocCpkOXVFmfYj7v5vZXF3ZNzMeyyJ6OzGfDTEEOm2CWA8KzZ578eA7j5VPCLOdGjOU8sGgi8BYw==" 73 | }, 74 | "crypto-js": { 75 | "version": "3.1.9-1", 76 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", 77 | "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" 78 | }, 79 | "debug": { 80 | "version": "4.1.1", 81 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 82 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 83 | "requires": { 84 | "ms": "^2.1.1" 85 | } 86 | }, 87 | "end-of-stream": { 88 | "version": "1.4.1", 89 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", 90 | "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", 91 | "requires": { 92 | "once": "^1.4.0" 93 | } 94 | }, 95 | "execa": { 96 | "version": "1.0.0", 97 | "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", 98 | "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", 99 | "requires": { 100 | "cross-spawn": "^6.0.0", 101 | "get-stream": "^4.0.0", 102 | "is-stream": "^1.1.0", 103 | "npm-run-path": "^2.0.0", 104 | "p-finally": "^1.0.0", 105 | "signal-exit": "^3.0.0", 106 | "strip-eof": "^1.0.0" 107 | } 108 | }, 109 | "fast-json-stable-stringify": { 110 | "version": "2.0.0", 111 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 112 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 113 | }, 114 | "get-stream": { 115 | "version": "4.1.0", 116 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 117 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 118 | "requires": { 119 | "pump": "^3.0.0" 120 | } 121 | }, 122 | "is-stream": { 123 | "version": "1.1.0", 124 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 125 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 126 | }, 127 | "isexe": { 128 | "version": "2.0.0", 129 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 130 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 131 | }, 132 | "macos-release": { 133 | "version": "2.3.0", 134 | "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", 135 | "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==" 136 | }, 137 | "ms": { 138 | "version": "2.1.2", 139 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 140 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 141 | }, 142 | "nice-try": { 143 | "version": "1.0.5", 144 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 145 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" 146 | }, 147 | "node-abort-controller": { 148 | "version": "1.0.3", 149 | "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-1.0.3.tgz", 150 | "integrity": "sha512-w07Dwqd/SWv9Lqrlhlx3mo4i4EWsuN3majbIIj4d6twBWGZUKtB9zvT9W+D5Rko56uas55CLO0YZ4zMrf6AKMw==" 151 | }, 152 | "node-fetch": { 153 | "version": "2.6.0", 154 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 155 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 156 | }, 157 | "npm-run-path": { 158 | "version": "2.0.2", 159 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 160 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 161 | "requires": { 162 | "path-key": "^2.0.0" 163 | } 164 | }, 165 | "once": { 166 | "version": "1.4.0", 167 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 168 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 169 | "requires": { 170 | "wrappy": "1" 171 | } 172 | }, 173 | "os-name": { 174 | "version": "3.1.0", 175 | "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", 176 | "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", 177 | "requires": { 178 | "macos-release": "^2.2.0", 179 | "windows-release": "^3.1.0" 180 | } 181 | }, 182 | "p-finally": { 183 | "version": "1.0.0", 184 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 185 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" 186 | }, 187 | "path-key": { 188 | "version": "2.0.1", 189 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 190 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" 191 | }, 192 | "priorityqueuejs": { 193 | "version": "1.0.0", 194 | "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", 195 | "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg=" 196 | }, 197 | "pump": { 198 | "version": "3.0.0", 199 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 200 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 201 | "requires": { 202 | "end-of-stream": "^1.1.0", 203 | "once": "^1.3.1" 204 | } 205 | }, 206 | "semaphore": { 207 | "version": "1.0.5", 208 | "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.0.5.tgz", 209 | "integrity": "sha1-tJJXbmavGT25XWXiXsU/Xxl5jWA=" 210 | }, 211 | "semver": { 212 | "version": "5.7.1", 213 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 214 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 215 | }, 216 | "shebang-command": { 217 | "version": "1.2.0", 218 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 219 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 220 | "requires": { 221 | "shebang-regex": "^1.0.0" 222 | } 223 | }, 224 | "shebang-regex": { 225 | "version": "1.0.0", 226 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 227 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 228 | }, 229 | "signal-exit": { 230 | "version": "3.0.2", 231 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 232 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 233 | }, 234 | "strip-eof": { 235 | "version": "1.0.0", 236 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 237 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" 238 | }, 239 | "tslib": { 240 | "version": "1.10.0", 241 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 242 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 243 | }, 244 | "typescript": { 245 | "version": "3.3.3", 246 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3.tgz", 247 | "integrity": "sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A==", 248 | "dev": true 249 | }, 250 | "universal-user-agent": { 251 | "version": "2.1.0", 252 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.1.0.tgz", 253 | "integrity": "sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q==", 254 | "requires": { 255 | "os-name": "^3.0.0" 256 | } 257 | }, 258 | "uuid": { 259 | "version": "3.3.2", 260 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 261 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 262 | }, 263 | "which": { 264 | "version": "1.3.1", 265 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 266 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 267 | "requires": { 268 | "isexe": "^2.0.0" 269 | } 270 | }, 271 | "windows-release": { 272 | "version": "3.2.0", 273 | "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", 274 | "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", 275 | "requires": { 276 | "execa": "^1.0.0" 277 | } 278 | }, 279 | "wrappy": { 280 | "version": "1.0.2", 281 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 282 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "prestart": "npm run build && func extensions install", 9 | "start:host": "func start", 10 | "start": "npm run start:host & npm run watch", 11 | "build:production": "npm run prestart && npm prune --production", 12 | "test": "echo \"No tests yet...\"" 13 | }, 14 | "dependencies": { 15 | "@azure/cosmos": "^3.2.0" 16 | }, 17 | "devDependencies": { 18 | "@azure/functions": "^1.0.1-beta2", 19 | "typescript": "^3.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /functions/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /functions/services/config.ts: -------------------------------------------------------------------------------- 1 | import { CosmosClient } from '@azure/cosmos'; 2 | 3 | const endpoint = process.env.CORE_API_URL; 4 | const masterKey = process.env.CORE_API_KEY; 5 | const databaseDefName = 'hero-db'; 6 | const heroContainerName = 'heroes'; 7 | const villainContainerName = 'villains'; 8 | 9 | const client = new CosmosClient({ endpoint, key: masterKey }); 10 | 11 | const containers = { 12 | heroes: client.database(databaseDefName).container(heroContainerName), 13 | villains: client.database(databaseDefName).container(villainContainerName) 14 | }; 15 | 16 | export default containers; 17 | -------------------------------------------------------------------------------- /functions/services/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@azure/functions'; 2 | import containers from './config'; 3 | const { heroes: container } = containers; 4 | 5 | async function getHeroes({ req, res }: Context) { 6 | try { 7 | const { resources: heroes } = await container.items.readAll().fetchAll(); 8 | res.status(200).json(heroes); 9 | } catch (error) { 10 | res.status(500).send(error); 11 | } 12 | } 13 | 14 | async function postHero({ req, res }: Context) { 15 | const hero = { 16 | name: req.body.name, 17 | description: req.body.description, 18 | id: undefined 19 | }; 20 | hero.id = `Hero${hero.name}`; 21 | 22 | try { 23 | const { item } = await container.items.create(hero); 24 | const { resource } = await item.read(); 25 | res.status(201).json(resource); 26 | } catch (error) { 27 | res.status(500).send(error); 28 | } 29 | } 30 | 31 | async function putHero({ req, res }: Context) { 32 | const hero = { 33 | id: req.params.id, 34 | name: req.body.name, 35 | description: req.body.description 36 | }; 37 | 38 | try { 39 | const partitionKey = hero.id; 40 | const { resource } = await container.item(hero.id, partitionKey).replace(hero); 41 | res.status(200).json(resource); 42 | } catch (error) { 43 | res.status(500).send(error); 44 | } 45 | } 46 | 47 | async function deleteHero({ req, res }: Context) { 48 | const { id } = req.params; 49 | 50 | try { 51 | const partitionKey = id; 52 | const { resource } = await container.item(id, partitionKey).delete(); 53 | res.status(200).json(resource); 54 | } catch (error) { 55 | res.status(500).send(error); 56 | } 57 | } 58 | 59 | export default { getHeroes, postHero, putHero, deleteHero }; 60 | -------------------------------------------------------------------------------- /functions/services/index.ts: -------------------------------------------------------------------------------- 1 | import heroService from './hero.service'; 2 | import villainService from './villain.service'; 3 | 4 | export { heroService, villainService }; 5 | -------------------------------------------------------------------------------- /functions/services/villain.service.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@azure/functions'; 2 | import containers from './config'; 3 | const { villains: container } = containers; 4 | 5 | async function getVillains({ req, res }: Context) { 6 | try { 7 | const { resources: villains } = await container.items.readAll().fetchAll(); 8 | res.status(200).json(villains); 9 | } catch (error) { 10 | res.status(500).send(error); 11 | } 12 | } 13 | 14 | async function postVillain({ req, res }: Context) { 15 | const villain = { 16 | name: req.body.name, 17 | description: req.body.description, 18 | id: undefined 19 | }; 20 | villain.id = `Villain${villain.name}`; 21 | 22 | try { 23 | const { item } = await container.items.create(villain); 24 | const { resource } = await item.read(); 25 | res.status(201).json(resource); 26 | } catch (error) { 27 | res.status(500).send(error); 28 | } 29 | } 30 | 31 | async function putVillain({ req, res }: Context) { 32 | const villain = { 33 | id: req.params.id, 34 | name: req.body.name, 35 | description: req.body.description 36 | }; 37 | 38 | try { 39 | const partitionKey = villain.id; 40 | const { resource } = await container.item(villain.id, partitionKey).replace(villain); 41 | res.status(200).json(resource); 42 | } catch (error) { 43 | res.status(500).send(error); 44 | } 45 | } 46 | 47 | async function deleteVillain({ req, res }: Context) { 48 | const { id } = req.params; 49 | 50 | try { 51 | const partitionKey = id; 52 | const { resource } = await container.item(id, partitionKey).delete(); 53 | res.status(200).json(resource); 54 | } catch (error) { 55 | res.status(500).send(error); 56 | } 57 | } 58 | 59 | export default { getVillains, postVillain, putVillain, deleteVillain }; 60 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /functions/villains-delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["delete"], 10 | "route": "villains/{id}" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/villains-delete/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/villains-delete/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { villainService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await villainService.deleteVillain(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/villains-get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["get"], 10 | "route": "villains" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/villains-get/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/villains-get/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { villainService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await villainService.getVillains(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/villains-post/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["post"], 10 | "route": "villains" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/villains-post/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/villains-post/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { villainService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await villainService.postVillain(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /functions/villains-put/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": ["put"], 10 | "route": "villains/{id}" 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ], 18 | "scriptFile": "../dist/villains-put/index.js" 19 | } 20 | -------------------------------------------------------------------------------- /functions/villains-put/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'; 2 | import { villainService } from '../services'; 3 | 4 | const httpTrigger: AzureFunction = async function(context: Context, req: HttpRequest): Promise { 5 | await villainService.putVillain(context); 6 | }; 7 | 8 | export default httpTrigger; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroes-angular-serverless", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --proxy-config src/proxy.conf.json", 7 | "build": "ng build --prod", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "node:build-ts": "tsc -p server", 11 | "node:watch-ts": "tsc -w -p server", 12 | "node:lint": "npm run node:build-ts && eslint \"server/**/*.ts\" --quiet --fix", 13 | "node:debug": "WWW=./dist/heroes-angular nodemon -r dotenv/config --inspect=9229 server/dist/index.js", 14 | "node:start": "WWW=./dist/heroes-angular node -r dotenv/config server/dist/index.js", 15 | "node-ng:build": "concurrently \"npm run node:build-ts\" \"npm run build\"", 16 | "node-ng:debug": "concurrently \"npm run node:debug\" \"npm run start\"" 17 | }, 18 | "private": true, 19 | "dependencies": { 20 | "@angular/animations": "~9.0.1", 21 | "@angular/common": "~9.0.1", 22 | "@angular/compiler": "~9.0.1", 23 | "@angular/core": "~9.0.1", 24 | "@angular/forms": "~9.0.1", 25 | "@angular/platform-browser": "~9.0.1", 26 | "@angular/platform-browser-dynamic": "~9.0.1", 27 | "@angular/router": "~9.0.1", 28 | "@azure/cosmos": "^3.2.0", 29 | "@fortawesome/angular-fontawesome": "^0.5.0", 30 | "@fortawesome/fontawesome-free": "^5.10.2", 31 | "@fortawesome/fontawesome-svg-core": "^1.2.22", 32 | "@fortawesome/free-solid-svg-icons": "^5.10.2", 33 | "@ngrx/data": "^8.2.5", 34 | "@ngrx/effects": "^8.2.5", 35 | "@ngrx/entity": "^8.2.5", 36 | "@ngrx/store": "^8.2.5", 37 | "@ngrx/store-devtools": "^8.2.5", 38 | "angular-font-awesome": "^3.1.2", 39 | "body-parser": "^1.19.0", 40 | "bulma": "^0.7.5", 41 | "core-js": "^2.6.9", 42 | "express": "^4.17.1", 43 | "font-awesome": "^4.7.0", 44 | "rxjs": "~6.5.4", 45 | "tslib": "^1.10.0", 46 | "zone.js": "~0.10.2" 47 | }, 48 | "devDependencies": { 49 | "@angular-devkit/build-angular": "~0.900.2", 50 | "@angular/cli": "^9.0.2", 51 | "@angular/compiler-cli": "~9.0.1", 52 | "@angular/language-service": "~9.0.1", 53 | "@types/body-parser": "^1.17.1", 54 | "@types/dotenv": "^6.1.1", 55 | "@types/express": "^4.17.1", 56 | "@types/jasmine": "~2.8.8", 57 | "@types/jasminewd2": "~2.0.3", 58 | "@types/node": "^12.11.1", 59 | "@typescript-eslint/eslint-plugin": "^2.2.0", 60 | "@typescript-eslint/parser": "^2.2.0", 61 | "codelyzer": "^5.1.2", 62 | "concurrently": "^4.1.2", 63 | "dotenv": "^7.0.0", 64 | "eslint": "^6.3.0", 65 | "jasmine-core": "~2.99.1", 66 | "jasmine-spec-reporter": "~4.2.1", 67 | "karma": "~4.0.0", 68 | "karma-chrome-launcher": "~2.2.0", 69 | "karma-coverage-istanbul-reporter": "^2.0.6", 70 | "karma-jasmine": "~1.1.2", 71 | "karma-jasmine-html-reporter": "^0.2.2", 72 | "nodemon": "^1.19.2", 73 | "protractor": "~5.4.0", 74 | "ts-node": "^7.0.1", 75 | "typescript": "~3.7.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { start } from './server'; 2 | 3 | start(); 4 | -------------------------------------------------------------------------------- /server/routes/hero.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { heroService } from '../services'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/heroes', (req, res) => { 7 | heroService.getHeroes(req, res); 8 | }); 9 | 10 | router.post('/heroes', (req, res) => { 11 | heroService.postHero(req, res); 12 | }); 13 | 14 | router.put('/heroes/:id', (req, res) => { 15 | heroService.putHero(req, res); 16 | }); 17 | 18 | router.delete('/heroes/:id', (req, res) => { 19 | heroService.deleteHero(req, res); 20 | }); 21 | 22 | // TODO: example of SQL query 23 | // Learn more here: https://www.documentdb.com/sql/demo 24 | // router.get('/hero/querybyname/:name', (req, res) => { 25 | // heroService.queryHeroesNyName(req, res); 26 | // }); 27 | 28 | export const heroRoutes = router; 29 | -------------------------------------------------------------------------------- /server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { heroRoutes } from './hero.routes'; 6 | import { villainRoutes } from './villain.routes'; 7 | 8 | router.use('/', heroRoutes); 9 | router.use('/', villainRoutes); 10 | 11 | export { router }; 12 | -------------------------------------------------------------------------------- /server/routes/villain.routes.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { villainService } from '../services'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/villains', (req, res) => { 7 | villainService.getVillains(req, res); 8 | }); 9 | 10 | router.post('/villains', (req, res) => { 11 | villainService.postVillain(req, res); 12 | }); 13 | 14 | router.put('/villains/:id', (req, res) => { 15 | villainService.putVillain(req, res); 16 | }); 17 | 18 | router.delete('/villains/:id', (req, res) => { 19 | villainService.deleteVillain(req, res); 20 | }); 21 | 22 | export const villainRoutes = router; 23 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { router } from './routes'; 4 | 5 | const captains = console; 6 | 7 | function start() { 8 | if (!process.env.NODE_ENV || !process.env.PORT) { 9 | captains.error('ENV variables are missing.', 'Verify that you have set them directly or in a .env file.'); 10 | process.exit(1); 11 | } else { 12 | captains.log('Using ENV variables'); 13 | } 14 | 15 | const app = express(); 16 | const port = process.env.PORT || 7627; 17 | const www = process.env.WWW || './'; 18 | app.use(bodyParser.json()); 19 | app.use(bodyParser.urlencoded({ extended: false })); 20 | 21 | app.use(express.static(www)); 22 | captains.log(`serving ${www}`); 23 | app.use('/api', router); 24 | app.get('*', (req, res) => { 25 | res.sendFile('index.html', { root: www }); 26 | }); 27 | app.listen(port, () => captains.log(`listening on http://localhost:${port}`)); 28 | } 29 | 30 | export { start }; 31 | -------------------------------------------------------------------------------- /server/services/config.ts: -------------------------------------------------------------------------------- 1 | import { CosmosClient } from '@azure/cosmos'; 2 | 3 | const endpoint = process.env.CORE_API_URL; 4 | const masterKey = process.env.CORE_API_KEY; 5 | const databaseDefName = 'hero-db'; 6 | const heroContainerName = 'heroes'; 7 | const villainContainerName = 'villains'; 8 | 9 | const client = new CosmosClient({ endpoint, key: masterKey }); 10 | 11 | const containers = { 12 | heroes: client.database(databaseDefName).container(heroContainerName), 13 | villains: client.database(databaseDefName).container(villainContainerName) 14 | }; 15 | 16 | export default containers; 17 | -------------------------------------------------------------------------------- /server/services/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import containers from './config'; 3 | const { heroes: container } = containers; 4 | 5 | async function getHeroes(req: Request, res: Response) { 6 | try { 7 | const { resources: heroes } = await container.items.readAll().fetchAll(); 8 | res.status(200).json(heroes); 9 | } catch (error) { 10 | res.status(500).send(error); 11 | } 12 | } 13 | 14 | async function postHero(req: Request, res: Response) { 15 | const hero = { 16 | name: req.body.name, 17 | description: req.body.description, 18 | id: undefined 19 | }; 20 | hero.id = `Hero${hero.name}`; 21 | 22 | try { 23 | const { item } = await container.items.create(hero); 24 | const { resource } = await item.read(); 25 | res.status(201).json(resource); 26 | } catch (error) { 27 | res.status(500).send(error); 28 | } 29 | } 30 | 31 | async function putHero(req: Request, res: Response) { 32 | const hero = { 33 | id: req.params.id, 34 | name: req.body.name, 35 | description: req.body.description 36 | }; 37 | 38 | try { 39 | const partitionKey = hero.id; 40 | const { resource } = await container.item(hero.id, partitionKey).replace(hero); 41 | res.status(200).json(resource); 42 | } catch (error) { 43 | res.status(500).send(error); 44 | } 45 | } 46 | 47 | async function deleteHero(req: Request, res: Response) { 48 | const { id } = req.params; 49 | 50 | try { 51 | const partitionKey = id; 52 | const { resource } = await container.item(id, partitionKey).delete(); 53 | res.status(200).json(resource); 54 | } catch (error) { 55 | res.status(500).send(error); 56 | } 57 | } 58 | 59 | export default { getHeroes, postHero, putHero, deleteHero }; 60 | -------------------------------------------------------------------------------- /server/services/index.ts: -------------------------------------------------------------------------------- 1 | import heroService from './hero.service'; 2 | import villainService from './villain.service'; 3 | 4 | export { heroService, villainService }; 5 | -------------------------------------------------------------------------------- /server/services/villain.service.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import containers from './config'; 3 | const { villains: container } = containers; 4 | 5 | async function getVillains(req: Request, res: Response) { 6 | try { 7 | const { resources: villains } = await container.items.readAll().fetchAll(); 8 | res.status(200).json(villains); 9 | } catch (error) { 10 | res.status(500).send(error); 11 | } 12 | } 13 | 14 | async function postVillain(req: Request, res: Response) { 15 | const villain = { 16 | name: req.body.name, 17 | description: req.body.description, 18 | id: undefined 19 | }; 20 | villain.id = `Villain${villain.name}`; 21 | 22 | try { 23 | const { item } = await container.items.create(villain); 24 | const { resource } = await item.read(); 25 | res.status(201).json(resource); 26 | } catch (error) { 27 | res.status(500).send(error); 28 | } 29 | } 30 | 31 | async function putVillain(req: Request, res: Response) { 32 | const villain = { 33 | id: req.params.id, 34 | name: req.body.name, 35 | description: req.body.description 36 | }; 37 | 38 | try { 39 | const partitionKey = villain.id; 40 | const { resource } = await container.item(villain.id, partitionKey).replace(villain); 41 | res.status(200).json(resource); 42 | } catch (error) { 43 | res.status(500).send(error); 44 | } 45 | } 46 | 47 | async function deleteVillain(req: Request, res: Response) { 48 | const { id } = req.params; 49 | 50 | try { 51 | const partitionKey = id; 52 | const { resource } = await container.item(id, partitionKey).delete(); 53 | res.status(200).json(resource); 54 | } catch (error) { 55 | res.status(500).send(error); 56 | } 57 | } 58 | 59 | export default { getVillains, postVillain, putVillain, deleteVillain }; 60 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "target": "es6", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "*": ["node_modules/*", "src/types/*"] 14 | } 15 | }, 16 | "include": ["./**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about', 5 | template: ` 6 |
7 |
8 |

Tour of Heroes

9 |

10 | This project was created to help represent a fundamental app written with Angular. The heroes and villains 11 | theme is used throughout the app. 12 |

13 |

by John Papa

14 |
15 |

Why

16 |

17 | I love JavaScript and the Web! One of the most common questions I hear is "which framework is best?". I like 18 | to flip this around and ask you "which is best for you?". The best way to know this is to try it for yourself. 19 | I'll follow up with some articles on my experiences with these frameworks but in the meantime, please try it 20 | for yourself to gain your own experience with each. 21 |

22 |
23 |

Comparative Apps

24 | 35 |
36 |

Live Demos

37 |

38 | Hosted in 39 | Azure 40 |

41 | 42 | 53 |
54 |
55 | ` 56 | }) 57 | export class AboutComponent {} 58 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NotFoundComponent } from './core'; 4 | import { AboutComponent } from './about.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', pathMatch: 'full', redirectTo: 'heroes' }, 8 | { 9 | path: 'heroes', 10 | loadChildren: () => import('./heroes/heroes.module').then(m => m.HeroesModule) 11 | }, 12 | { 13 | path: 'villains', 14 | loadChildren: () => import('./villains/villains.module').then(m => m.VillainsModule) 15 | }, 16 | { path: 'about', component: AboutComponent }, 17 | { path: '**', component: NotFoundComponent } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forRoot(routes)], 22 | exports: [RouterModule] 23 | }) 24 | export class AppRoutingModule {} 25 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/heroes-angular-serverless/84e26f15a7f588be255f9215f7e8f1898f49d069/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'heroes-angular'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app.title).toEqual('heroes-angular'); 27 | }); 28 | 29 | it('should render title in a h1 tag', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to heroes-angular!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { CoreModule } from './core/core.module'; 8 | import { AppStoreModule } from './store/store.module'; 9 | import { AboutComponent } from './about.component'; 10 | import { EntityDataModule } from '@ngrx/data'; 11 | 12 | @NgModule({ 13 | declarations: [AppComponent, AboutComponent], 14 | imports: [BrowserModule, HttpClientModule, CoreModule, AppRoutingModule, AppStoreModule, EntityDataModule], 15 | providers: [], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | import { HeaderBarBrandComponent } from './header-bar-brand.component'; 7 | import { HeaderBarLinksComponent } from './header-bar-links.component'; 8 | import { HeaderBarComponent } from './header-bar.component'; 9 | import { NavComponent } from './nav.component'; 10 | import { NotFoundComponent } from './not-found.component'; 11 | 12 | const components = [ 13 | NavComponent, 14 | HeaderBarComponent, 15 | HeaderBarBrandComponent, 16 | HeaderBarLinksComponent, 17 | NotFoundComponent 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [ 22 | CommonModule, 23 | FontAwesomeModule, 24 | RouterModule, // because we use and routerLink 25 | SharedModule 26 | ], 27 | exports: [FontAwesomeModule, components], 28 | declarations: [components] 29 | }) 30 | export class CoreModule {} 31 | -------------------------------------------------------------------------------- /src/app/core/header-bar-brand.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header-bar-brand', 5 | template: ` 6 | 29 | ` 30 | }) 31 | export class HeaderBarBrandComponent {} 32 | -------------------------------------------------------------------------------- /src/app/core/header-bar-links.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header-bar-links', 5 | template: ` 6 | 20 | `, 21 | styles: [ 22 | ` 23 | :host { 24 | width: 100%; 25 | } 26 | ` 27 | ] 28 | }) 29 | export class HeaderBarLinksComponent {} 30 | -------------------------------------------------------------------------------- /src/app/core/header-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header-bar', 5 | template: ` 6 |
7 | 11 |
12 | ` 13 | }) 14 | export class HeaderBarComponent {} 15 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core.module'; 2 | export * from './header-bar.component'; 3 | export * from './model'; 4 | export * from './nav.component'; 5 | export * from './not-found.component'; 6 | -------------------------------------------------------------------------------- /src/app/core/model/hero.ts: -------------------------------------------------------------------------------- 1 | export class Hero { 2 | id: string; 3 | name: string; 4 | description: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hero'; 2 | export * from './villain'; 3 | -------------------------------------------------------------------------------- /src/app/core/model/villain.ts: -------------------------------------------------------------------------------- 1 | export class Villain { 2 | id: string; 3 | name: string; 4 | description: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/core/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { faCoffee } from '@fortawesome/free-solid-svg-icons'; 3 | 4 | @Component({ 5 | selector: 'app-nav', 6 | template: ` 7 | 21 | ` 22 | }) 23 | export class NavComponent implements OnInit { 24 | constructor() {} 25 | 26 | ngOnInit() {} 27 | } 28 | -------------------------------------------------------------------------------- /src/app/core/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | template: ` 6 |
7 |
8 |   9 | These aren't the bits you're looking for 10 |
11 |
12 | `, 13 | }) 14 | export class NotFoundComponent implements OnInit { 15 | constructor() {} 16 | 17 | ngOnInit() {} 18 | } 19 | -------------------------------------------------------------------------------- /src/app/heroes/hero-detail/hero-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{editingHero.name}} 5 |   6 |

7 |
8 |
9 |
10 |
11 | 14 | 15 |
16 |
17 | 20 | 21 |
22 |
23 | 26 | 27 |
28 |
29 |
30 |
31 | 33 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/heroes/hero-detail/hero-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | EventEmitter, 5 | OnChanges, 6 | Output, 7 | SimpleChanges, 8 | ChangeDetectionStrategy 9 | } from '@angular/core'; 10 | 11 | import { Hero } from '../../core'; 12 | 13 | @Component({ 14 | selector: 'app-hero-detail', 15 | templateUrl: './hero-detail.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class HeroDetailComponent implements OnChanges { 19 | @Input() hero: Hero; 20 | @Output() unselect = new EventEmitter(); 21 | @Output() save = new EventEmitter(); 22 | 23 | addMode = false; 24 | editingHero: Hero; 25 | 26 | ngOnChanges(changes: SimpleChanges) { 27 | if (this.hero && this.hero.id) { 28 | this.editingHero = { ...this.hero }; 29 | this.addMode = false; 30 | } else { 31 | this.editingHero = { id: undefined, name: '', description: '' }; 32 | this.addMode = true; 33 | } 34 | } 35 | 36 | clear() { 37 | this.unselect.emit(); 38 | } 39 | 40 | saveHero() { 41 | this.save.emit(this.editingHero); 42 | this.clear(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/heroes/hero-list/hero-list.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 |
    4 | 6 |
    7 | 8 | 9 |
    10 |
    11 |
  • 12 |
13 | -------------------------------------------------------------------------------- /src/app/heroes/hero-list/hero-list.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | Output, 6 | ChangeDetectionStrategy 7 | } from '@angular/core'; 8 | import { Hero } from '../../core'; 9 | 10 | @Component({ 11 | selector: 'app-hero-list', 12 | templateUrl: './hero-list.component.html', 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class HeroListComponent { 16 | @Input() heroes: Hero[]; 17 | @Input() selectedHero: Hero; 18 | @Output() deleted = new EventEmitter(); 19 | @Output() selected = new EventEmitter(); 20 | 21 | selectHero(hero: Hero) { 22 | this.selected.emit(hero); 23 | } 24 | 25 | deleteHero(hero: Hero) { 26 | this.deleted.emit(hero); 27 | } 28 | 29 | byId(hero: Hero) { 30 | return hero.id; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/heroes/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | EntityCollectionServiceBase, 4 | EntityCollectionServiceElementsFactory 5 | } from '@ngrx/data'; 6 | import { Hero } from '../core'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class HeroService extends EntityCollectionServiceBase { 10 | constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { 11 | super('Hero', serviceElementsFactory); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/heroes/heroes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { CommonModule } from '@angular/common'; 4 | import { HeroesComponent } from './heroes/heroes.component'; 5 | import { HeroListComponent } from './hero-list/hero-list.component'; 6 | import { HeroDetailComponent } from './hero-detail/hero-detail.component'; 7 | import { SharedModule } from '../shared/shared.module'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: HeroesComponent, 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [CommonModule, RouterModule.forChild(routes), SharedModule], 18 | exports: [RouterModule, HeroesComponent], 19 | declarations: [HeroesComponent, HeroListComponent, HeroDetailComponent] 20 | }) 21 | export class HeroesModule {} 22 | -------------------------------------------------------------------------------- /src/app/heroes/heroes/heroes.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/heroes/heroes/heroes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Hero } from '../../core'; 4 | import { HeroService } from '../hero.service'; 5 | 6 | @Component({ 7 | selector: 'app-heroes', 8 | templateUrl: './heroes.component.html' 9 | }) 10 | export class HeroesComponent implements OnInit { 11 | selected: Hero; 12 | heroes$: Observable; 13 | message = '?'; 14 | heroToDelete: Hero; 15 | showModal = false; 16 | 17 | constructor( 18 | private heroService: HeroService // , private modalService: ModalService 19 | ) { 20 | this.heroes$ = heroService.entities$; 21 | } 22 | 23 | ngOnInit() { 24 | this.getHeroes(); 25 | } 26 | 27 | add(hero: Hero) { 28 | this.heroService.add(hero); 29 | } 30 | 31 | askToDelete(hero: Hero) { 32 | this.heroToDelete = hero; 33 | this.showModal = true; 34 | if (this.heroToDelete.name) { 35 | this.message = `Would you like to delete ${this.heroToDelete.name}?`; 36 | } 37 | } 38 | 39 | clear() { 40 | this.selected = null; 41 | } 42 | 43 | closeModal() { 44 | this.showModal = false; 45 | } 46 | 47 | deleteHero() { 48 | this.closeModal(); 49 | if (this.heroToDelete) { 50 | this.heroService 51 | .delete(this.heroToDelete.id) 52 | .subscribe(() => (this.heroToDelete = null)); 53 | } 54 | this.clear(); 55 | } 56 | 57 | enableAddMode() { 58 | this.selected = {} as any; 59 | } 60 | 61 | getHeroes() { 62 | this.heroService.getAll(); 63 | this.clear(); 64 | } 65 | 66 | save(hero: Hero) { 67 | if (this.selected && this.selected.name) { 68 | this.update(hero); 69 | } else { 70 | this.add(hero); 71 | } 72 | } 73 | 74 | select(hero: Hero) { 75 | this.selected = hero; 76 | } 77 | 78 | update(hero: Hero) { 79 | this.heroService.update(hero); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/shared/button-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, EventEmitter, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-button-footer', 5 | template: ` 6 | 16 | ` 17 | }) 18 | export class ButtonFooterComponent { 19 | @Input() label; 20 | @Input() className; 21 | @Input() iconClasses; 22 | @Input() item; 23 | @Input() dataId; 24 | 25 | @Output() clicked = new EventEmitter(); 26 | 27 | handleClick() { 28 | this.clicked.emit(this.item); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/card-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-card-content', 5 | template: ` 6 |
7 |
8 |
{{ name }}
9 |
{{ description }}
10 |
11 |
12 | ` 13 | }) 14 | export class CardContentComponent implements OnInit { 15 | @Input() name; 16 | @Input() description; 17 | 18 | ngOnInit() {} 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/list-header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-list-header', 5 | template: ` 6 |
7 | 8 |

{{title}}

9 |
10 | 17 | 24 |
25 | ` 26 | }) 27 | export class ListHeaderComponent implements OnInit { 28 | @Input() title: string; 29 | @Output() add = new EventEmitter(); 30 | @Output() refresh = new EventEmitter(); 31 | 32 | ngOnInit() {} 33 | 34 | handleAdd() { 35 | this.add.emit(); 36 | } 37 | handleRefresh() { 38 | this.refresh.emit(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core'; 2 | import { Hero } from '../core'; 3 | 4 | @Component({ 5 | selector: 'app-modal', 6 | template: ` 7 | 22 | ` 23 | }) 24 | export class ModalComponent implements OnInit { 25 | @Input() message; 26 | @Input() isOpen = false; 27 | @Output() handleYes = new EventEmitter(); 28 | @Output() handleNo = new EventEmitter(); 29 | 30 | ngOnInit() {} 31 | 32 | onNo = () => { 33 | this.handleNo.emit(); 34 | } 35 | 36 | onYes = () => { 37 | this.handleYes.emit(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { ListHeaderComponent } from './list-header.component'; 5 | import { CardContentComponent } from './card-content.component'; 6 | import { ButtonFooterComponent } from './button-footer.component'; 7 | import { ModalComponent } from './modal.component'; 8 | 9 | const components = [ 10 | ButtonFooterComponent, 11 | CardContentComponent, 12 | ListHeaderComponent, 13 | ModalComponent 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [CommonModule, FormsModule, ReactiveFormsModule], 18 | declarations: [components], 19 | exports: [components, FormsModule, ReactiveFormsModule] 20 | }) 21 | export class SharedModule {} 22 | -------------------------------------------------------------------------------- /src/app/store/config.ts: -------------------------------------------------------------------------------- 1 | import { DefaultDataServiceConfig } from '@ngrx/data'; 2 | import { environment } from './../../environments/environment'; 3 | 4 | const root = environment.API; 5 | 6 | export const defaultDataServiceConfig: DefaultDataServiceConfig = { 7 | root, // default root path to the server's web api 8 | 9 | entityHttpResourceUrls: { 10 | Hero: { 11 | // You must specify the root as part of the resource URL. 12 | entityResourceUrl: `${root}/heroes/`, 13 | collectionResourceUrl: `${root}/heroes/` 14 | }, 15 | Villain: { 16 | entityResourceUrl: `${root}/villains/`, 17 | collectionResourceUrl: `${root}/villains/` 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/store/entity-metadata.ts: -------------------------------------------------------------------------------- 1 | import { EntityMetadataMap } from '@ngrx/data'; 2 | 3 | const entityMetadata: EntityMetadataMap = { 4 | Hero: {}, 5 | Villain: {} 6 | }; 7 | 8 | const pluralNames = { Hero: 'Heroes' }; 9 | 10 | export const entityConfig = { 11 | entityMetadata, 12 | pluralNames 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { EffectsModule } from '@ngrx/effects'; 3 | import { StoreModule } from '@ngrx/store'; 4 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 5 | import { DefaultDataServiceConfig, EntityDataModule } from '@ngrx/data'; 6 | import { environment } from '../../environments/environment'; 7 | import { defaultDataServiceConfig } from './config'; 8 | import { entityConfig } from './entity-metadata'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | StoreModule.forRoot({}), 13 | EffectsModule.forRoot([]), 14 | environment.production ? [] : StoreDevtoolsModule.instrument(), 15 | EntityDataModule.forRoot(entityConfig) 16 | ], 17 | providers: [{ provide: DefaultDataServiceConfig, useValue: defaultDataServiceConfig }] 18 | }) 19 | export class AppStoreModule {} 20 | -------------------------------------------------------------------------------- /src/app/villains/villain-detail/villain-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{editingVillain.name}} 5 |   6 |

7 |
8 |
9 |
10 |
11 | 14 | 15 |
16 |
17 | 20 | 21 |
22 |
23 | 26 | 27 |
28 |
29 |
30 |
31 | 33 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/villains/villain-detail/villain-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | EventEmitter, 5 | OnChanges, 6 | Output, 7 | SimpleChanges, 8 | ChangeDetectionStrategy 9 | } from '@angular/core'; 10 | 11 | import { Villain } from '../../core'; 12 | 13 | @Component({ 14 | selector: 'app-villain-detail', 15 | templateUrl: './villain-detail.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class VillainDetailComponent implements OnChanges { 19 | @Input() villain: Villain; 20 | @Output() unselect = new EventEmitter(); 21 | @Output() save = new EventEmitter(); 22 | 23 | addMode = false; 24 | editingVillain: Villain; 25 | 26 | ngOnChanges(changes: SimpleChanges) { 27 | if (this.villain && this.villain.id) { 28 | this.editingVillain = { ...this.villain }; 29 | this.addMode = false; 30 | } else { 31 | this.editingVillain = { id: undefined, name: '', description: '' }; 32 | this.addMode = true; 33 | } 34 | } 35 | 36 | clear() { 37 | this.unselect.emit(); 38 | } 39 | 40 | saveVillain() { 41 | this.save.emit(this.editingVillain); 42 | this.clear(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/villains/villain-list/villain-list.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 |
    4 | 6 |
    7 | 13 | 19 |
    20 |
    21 |
  • 22 |
23 | -------------------------------------------------------------------------------- /src/app/villains/villain-list/villain-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Villain } from '../../core'; 3 | 4 | @Component({ 5 | selector: 'app-villain-list', 6 | templateUrl: './villain-list.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class VillainListComponent { 10 | @Input() villains: Villain[]; 11 | @Input() selectedVillain: Villain; 12 | @Output() deleted = new EventEmitter(); 13 | @Output() selected = new EventEmitter(); 14 | 15 | byId(villain: Villain) { 16 | return villain.id; 17 | } 18 | 19 | selectVillain(villain: Villain) { 20 | this.selected.emit(villain); 21 | } 22 | 23 | deleteVillain(villain: Villain) { 24 | this.deleted.emit(villain); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/villains/villain.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '@ngrx/data'; 3 | import { Villain } from '../core'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class VillainService extends EntityCollectionServiceBase { 7 | constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { 8 | super('Villain', serviceElementsFactory); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/villains/villains.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { SharedModule } from '../shared/shared.module'; 5 | import { VillainDetailComponent } from './villain-detail/villain-detail.component'; 6 | import { VillainListComponent } from './villain-list/villain-list.component'; 7 | import { VillainsComponent } from './villains/villains.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: VillainsComponent 13 | } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [CommonModule, RouterModule.forChild(routes), SharedModule], 18 | exports: [RouterModule, VillainsComponent], 19 | declarations: [ 20 | VillainsComponent, 21 | VillainListComponent, 22 | VillainDetailComponent 23 | ] 24 | }) 25 | export class VillainsModule {} 26 | -------------------------------------------------------------------------------- /src/app/villains/villains/villains.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/villains/villains/villains.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Villain } from '../../core'; 4 | import { VillainService } from '../villain.service'; 5 | 6 | @Component({ 7 | selector: 'app-villains', 8 | templateUrl: './villains.component.html' 9 | }) 10 | export class VillainsComponent implements OnInit { 11 | selected: Villain; 12 | villains$: Observable; 13 | message = '?'; 14 | villainToDelete: Villain; 15 | showModal = false; 16 | 17 | constructor( 18 | private villainService: VillainService // , private modalService: ModalService 19 | ) { 20 | this.villains$ = villainService.entities$; 21 | } 22 | 23 | ngOnInit() { 24 | this.getVillains(); 25 | } 26 | 27 | add(villain: Villain) { 28 | this.villainService.add(villain); 29 | } 30 | 31 | askToDelete(villain: Villain) { 32 | this.villainToDelete = villain; 33 | this.showModal = true; 34 | if (this.villainToDelete.name) { 35 | this.message = `Would you like to delete ${this.villainToDelete.name}?`; 36 | } 37 | } 38 | 39 | clear() { 40 | this.selected = null; 41 | } 42 | 43 | closeModal() { 44 | this.showModal = false; 45 | } 46 | 47 | deleteVillain() { 48 | this.closeModal(); 49 | if (this.villainToDelete) { 50 | this.villainService.delete(this.villainToDelete.id).subscribe(() => (this.villainToDelete = null)); 51 | } 52 | this.clear(); 53 | } 54 | 55 | enableAddMode() { 56 | this.selected = {} as any; 57 | } 58 | 59 | getVillains() { 60 | this.villainService.getAll(); 61 | this.clear(); 62 | } 63 | 64 | save(villain: Villain) { 65 | if (this.selected && this.selected.name) { 66 | this.update(villain); 67 | } else { 68 | this.add(villain); 69 | } 70 | } 71 | 72 | select(villain: Villain) { 73 | this.selected = villain; 74 | } 75 | 76 | update(villain: Villain) { 77 | this.villainService.update(villain); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/heroes-angular-serverless/84e26f15a7f588be255f9215f7e8f1898f49d069/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | API: 'api' 4 | // API: 'https://papa-heroes-node-api.azurewebsites.net/api' 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | API: 'api' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/heroes-angular-serverless/84e26f15a7f588be255f9215f7e8f1898f49d069/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Heroes Angular 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:7071", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | $vue: #42b883; 2 | $vue-light: #42b883; 3 | $angular: #b52e31; 4 | $angular-light: #eb7a7c; 5 | $react: #00b3e6; 6 | $react-light: #61dafb; 7 | $primary: $angular; 8 | $primary-light: $angular-light; 9 | $link: $primary; // #00b3e6; // #ff4081; 10 | 11 | $shade-light: #fafafa; 12 | 13 | @import 'bulma/bulma.sass'; 14 | 15 | .menu-list .router-link-active { 16 | color: #fff; 17 | background-color: $link; 18 | } 19 | 20 | .not-found { 21 | i { 22 | font-size: 20px; 23 | margin-right: 8px; 24 | } 25 | .title { 26 | letter-spacing: 0px; 27 | font-weight: normal; 28 | font-size: 24px; 29 | text-transform: none; 30 | } 31 | } 32 | 33 | header { 34 | font-weight: bold; 35 | font-family: Arial; 36 | span { 37 | letter-spacing: 0px; 38 | &.tour { 39 | color: #fff; 40 | } 41 | &.of { 42 | color: #ccc; 43 | } 44 | &.heroes { 45 | color: $primary-light; 46 | } 47 | } 48 | .navbar-item.nav-home { 49 | border: 3px solid transparent; 50 | border-radius: 0%; 51 | &:hover { 52 | border-right: 3px solid $primary-light; 53 | border-left: 3px solid $primary-light; 54 | } 55 | } 56 | .fab { 57 | font-size: 24px; 58 | &.js-logo { 59 | color: $primary-light; 60 | } 61 | } 62 | .buttons { 63 | i.fab { 64 | color: #fff; 65 | margin-left: 20px; 66 | margin-right: 10px; 67 | &:hover { 68 | color: $primary-light; 69 | } 70 | } 71 | } 72 | } 73 | 74 | .edit-detail { 75 | .input[readonly] { 76 | background-color: $shade-light; 77 | } 78 | } 79 | 80 | .content-title-group { 81 | margin-bottom: 16px; 82 | h2 { 83 | border-left: 16px solid $primary; 84 | border-bottom: 2px solid $primary; 85 | padding-left: 8px; 86 | padding-right: 16px; 87 | display: inline-block; 88 | text-transform: uppercase; 89 | color: #555; 90 | letter-spacing: 0px; 91 | &:hover { 92 | color: $link; 93 | } 94 | } 95 | button.button { 96 | border: 0; 97 | color: #999; 98 | &:hover { 99 | color: $link; 100 | } 101 | } 102 | } 103 | ul.list { 104 | box-shadow: none; 105 | } 106 | div.card-content { 107 | background-color: $shade-light; 108 | .name { 109 | font-size: 28px; 110 | color: #000; 111 | } 112 | .description { 113 | font-size: 20px; 114 | color: #999; 115 | } 116 | background-color: $shade-light; 117 | } 118 | .card { 119 | margin-bottom: 2em; 120 | } 121 | 122 | label.label { 123 | font-weight: normal; 124 | } 125 | 126 | p.card-header-title { 127 | background-color: $primary; 128 | text-transform: uppercase; 129 | letter-spacing: 4px; 130 | color: #fff; 131 | display: block; 132 | padding-left: 24px; 133 | } 134 | .card-footer button { 135 | font-size: 16px; 136 | i { 137 | margin-right: 10px; 138 | } 139 | color: #888; 140 | &:hover { 141 | color: $link; 142 | } 143 | } 144 | 145 | .modal-card-foot button { 146 | display: inline-block; 147 | width: 80px; 148 | } 149 | 150 | .modal-card-head, 151 | .modal-card-body { 152 | text-align: center; 153 | } 154 | 155 | .field { 156 | margin-bottom: 0.75rem; 157 | } 158 | 159 | .navbar-burger { 160 | margin-left: auto; 161 | } 162 | 163 | button.link { 164 | background: none; 165 | border: none; 166 | cursor: pointer; 167 | } 168 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "no-inputs-metadata-property": true, 122 | "no-outputs-metadata-property": true, 123 | "no-host-metadata-property": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-lifecycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------