├── src ├── assets │ ├── .gitkeep │ ├── images │ │ ├── male.png │ │ ├── favicon.ico │ │ ├── female.png │ │ └── people.png │ └── styles │ │ └── styles.css ├── styles.css ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── app │ ├── app.component.ts │ ├── shared │ │ ├── property-resolver.ts │ │ ├── pipes │ │ │ ├── trim.pipe.ts │ │ │ └── capitalize.pipe.ts │ │ ├── ensureModuleLoadedOnceGuard.ts │ │ ├── pagination │ │ │ ├── pagination.component.css │ │ │ ├── pagination.component.html │ │ │ └── pagination.component.ts │ │ ├── filter-textbox │ │ │ └── filter-textbox.component.ts │ │ ├── shared.module.ts │ │ ├── interfaces.ts │ │ └── validation.service.ts │ ├── core │ │ ├── trackby.service.ts │ │ ├── core.module.ts.httpmodule │ │ ├── data-filter.service.ts │ │ ├── sorter.ts │ │ ├── core.module.ts │ │ ├── data.service.ts │ │ └── data.service.ts.httpmodule │ ├── app.module.ts │ ├── customers │ │ ├── customers-grid.component.ts │ │ ├── customers.component.html │ │ ├── customers.component.ts │ │ ├── customers-grid.component.html │ │ ├── customer-edit.component.ts │ │ ├── customer-edit-reactive.component.html │ │ ├── customer-edit.component.html │ │ └── customer-edit-reactive.component.ts │ └── app-routing.module.ts ├── main.ts ├── test.ts ├── index.html └── polyfills.ts ├── public ├── styles.ef46db3751d8e999.css ├── favicon.ico ├── assets │ ├── images │ │ ├── male.png │ │ ├── favicon.ico │ │ ├── female.png │ │ └── people.png │ └── styles │ │ └── styles.css ├── runtime.f47bad7546ed9af5.js ├── index.html ├── 3rdpartylicenses.txt └── polyfills.0ed53563d957f923.js ├── config └── config.development.json ├── .vscode ├── settings.json └── launch.json ├── e2e ├── app.po.ts ├── tsconfig.e2e.json └── app.e2e-spec.ts ├── lib ├── configLoader.js ├── statesRepository.js ├── database.js ├── customersRepository.js └── dbSeeder.js ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── models ├── state.js └── customer.js ├── browserslist ├── tsconfig.json ├── .gitignore ├── controllers └── api │ ├── states │ └── states.controller.js │ ├── tokens │ └── tokens.controller.js │ └── customers │ └── customers.controller.js ├── docker-compose.yml ├── .docker ├── node.development.dockerfile └── useful-commands.md ├── karma.conf.js ├── package.json ├── routes └── router.js ├── README.md ├── angular.json └── server.js /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/styles.ef46db3751d8e999.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/src/assets/images/male.png -------------------------------------------------------------------------------- /public/assets/images/male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/public/assets/images/male.png -------------------------------------------------------------------------------- /src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/src/assets/images/female.png -------------------------------------------------------------------------------- /src/assets/images/people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/src/assets/images/people.png -------------------------------------------------------------------------------- /config/config.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseConfig": { 3 | "host": "localhost", 4 | "database": "customermanager" 5 | } 6 | } -------------------------------------------------------------------------------- /public/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/public/assets/images/favicon.ico -------------------------------------------------------------------------------- /public/assets/images/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/public/assets/images/female.png -------------------------------------------------------------------------------- /public/assets/images/people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanWahlin/AngularCLI-NodeJS-MongoDB-CustomersService/HEAD/public/assets/images/people.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/app/**/*.js.map": true, 5 | "**/app/**/*.js": true 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: `` 6 | }) 7 | export class AppComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/property-resolver.ts: -------------------------------------------------------------------------------- 1 | export class propertyResolver { 2 | static resolve(path: string, obj: any) { 3 | return path.split('.').reduce((prev, curr) => { 4 | return (prev ? prev[curr] : undefined) 5 | }, obj || self) 6 | } 7 | } -------------------------------------------------------------------------------- /e2e/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 | -------------------------------------------------------------------------------- /src/app/shared/pipes/trim.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({name: 'trim'}) 4 | export class TrimPipe implements PipeTransform { 5 | transform(value: any) { 6 | if (!value) { 7 | return ''; 8 | } 9 | return value.trim(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/app/core/trackby.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { ICustomer } from '../shared/interfaces'; 4 | 5 | @Injectable() 6 | export class TrackByService { 7 | 8 | customer(index: number, customer: ICustomer) { 9 | return customer._id; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/app/shared/ensureModuleLoadedOnceGuard.ts: -------------------------------------------------------------------------------- 1 | export class EnsureModuleLoadedOnceGuard { 2 | 3 | constructor(targetModule: any) { 4 | if (targetModule) { 5 | throw new Error(`${targetModule.constructor.name} has already been loaded. Import this module in the AppModule only.`); 6 | } 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /lib/configLoader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = 'development'; 4 | 5 | const env = process.env.NODE_ENV; 6 | 7 | console.log(`Node environment: ${env}`); 8 | console.log(`loading config.${env}.json`); 9 | 10 | module.exports = require(`../config/config.${env}.json`); -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.css: -------------------------------------------------------------------------------- 1 | .pagination>.active>a, .pagination>.active>a:focus, .pagination>.active>a:hover, .pagination>.active>span, .pagination>.active>span:focus, .pagination>.active>span:hover { 2 | background-color: #027FF4; 3 | border-color: #027FF4; 4 | } 5 | 6 | .pagination a { 7 | cursor: pointer; 8 | } -------------------------------------------------------------------------------- /src/app/shared/pipes/capitalize.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'capitalize' }) 4 | export class CapitalizePipe implements PipeTransform { 5 | 6 | transform(value: any) { 7 | if (value) { 8 | return value.charAt(0).toUpperCase() + value.slice(1); 9 | } 10 | return value; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('angular-cli-node-js-mongo-db-customers-service 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 app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /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/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /models/state.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | ObjectId = Schema.ObjectId; 4 | 5 | const StateSchema = new Schema({ 6 | id : { type : Number, required: true }, 7 | abbreviation : { type : String, required: true, trim: true }, 8 | name : { type : String, required: true, trim: true } 9 | }); 10 | 11 | module.exports = mongoose.model('State', StateSchema); 12 | 13 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { CoreModule } from './core/core.module'; 7 | import { SharedModule } from './shared/shared.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | AppRoutingModule, 13 | CoreModule, //Singleton objects 14 | SharedModule //Shared (multi-instance) objects 15 | ], 16 | declarations: [ AppComponent, AppRoutingModule.components ], 17 | bootstrap: [ AppComponent ] 18 | }) 19 | export class AppModule { } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | /.angular 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /src/app/shared/filter-textbox/filter-textbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-filter-textbox', 5 | template: ` 6 |
7 | Filter: 8 | 11 |
12 | ` 13 | }) 14 | export class FilterTextboxComponent { 15 | 16 | 17 | model: { filter: string } = { filter: null }; 18 | 19 | @Output() 20 | changed: EventEmitter = new EventEmitter(); 21 | 22 | filterChanged(event: any) { 23 | event.preventDefault(); 24 | this.changed.emit(this.model.filter); //Raise changed event 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /controllers/api/states/states.controller.js: -------------------------------------------------------------------------------- 1 | const statesRepo = require('../../../lib/statesRepository'), 2 | util = require('util'); 3 | 4 | class StatesController { 5 | 6 | constructor(router) { 7 | router.get('/', this.getStates.bind(this)); 8 | } 9 | 10 | getStates(req, res) { 11 | console.log('*** getStates'); 12 | 13 | statesRepo.getStates((err, data) => { 14 | if (err) { 15 | console.log('*** getStates error: ' + util.inspect(err)); 16 | res.json({ 17 | states: null 18 | }); 19 | } else { 20 | console.log('*** getStates ok'); 21 | res.json(data); 22 | } 23 | }); 24 | } 25 | 26 | } 27 | 28 | module.exports = StatesController; -------------------------------------------------------------------------------- /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 | 5 | import { PaginationComponent } from './pagination/pagination.component'; 6 | import { CapitalizePipe } from './pipes/capitalize.pipe'; 7 | import { TrimPipe } from './pipes/trim.pipe'; 8 | import { FilterTextboxComponent } from './filter-textbox/filter-textbox.component'; 9 | 10 | @NgModule({ 11 | imports: [ CommonModule, FormsModule, ReactiveFormsModule ], 12 | declarations: [CapitalizePipe, TrimPipe, FilterTextboxComponent, PaginationComponent ], 13 | exports: [ CommonModule, FormsModule, ReactiveFormsModule, CapitalizePipe, TrimPipe, FilterTextboxComponent, PaginationComponent ] 14 | }) 15 | export class SharedModule { } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/server.js", 12 | "cwd": "${workspaceRoot}", 13 | "outFiles": [], 14 | "sourceMaps": true 15 | }, 16 | { 17 | "type": "node", 18 | "request": "attach", 19 | "name": "Attach to Process", 20 | "port": 5858, 21 | "outFiles": [], 22 | "sourceMaps": true 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/app/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICustomer { 2 | _id?: string; 3 | firstName: string; 4 | lastName: string; 5 | email: string; 6 | address: string; 7 | city: string; 8 | state?: IState; 9 | stateId?: number; 10 | zip: number; 11 | gender: string; 12 | orderCount?: number; 13 | orders?: IOrder[]; 14 | orderTotal?: number; 15 | } 16 | 17 | export interface IState { 18 | abbreviation: string; 19 | name: string; 20 | } 21 | 22 | export interface IOrder { 23 | product: string; 24 | price: number; 25 | quantity: number; 26 | orderTotal?: number; 27 | } 28 | 29 | export interface IPagedResults { 30 | totalRecords: number; 31 | results: T; 32 | } 33 | 34 | export interface ICustomerResponse { 35 | customer: ICustomer; 36 | status: boolean; 37 | error: string; 38 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Run docker-compose build 2 | # Run docker-compose up 3 | # Live long and prosper 4 | 5 | version: '2' 6 | 7 | services: 8 | 9 | node: 10 | container_name: nodeapp 11 | image: nodeapp 12 | build: 13 | context: . 14 | dockerfile: .docker/node.development.dockerfile 15 | volumes: 16 | - .:/var/www/angularnoderestfulservice 17 | environment: 18 | - NODE_ENV=development 19 | ports: 20 | - "3000:3000" 21 | depends_on: 22 | - mongodb 23 | networks: 24 | - nodeapp-network 25 | 26 | #No authentication is provided here - just a demo! Read the Dockerfile 27 | #for more information about adding authentication. 28 | mongodb: 29 | container_name: mongodb 30 | image: mongo 31 | networks: 32 | - nodeapp-network 33 | 34 | networks: 35 | nodeapp-network: 36 | driver: bridge -------------------------------------------------------------------------------- /src/app/customers/customers-grid.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { ICustomer } from '../shared/interfaces'; 4 | import { Sorter } from '../core/sorter'; 5 | import { TrackByService } from '../core/trackby.service'; 6 | 7 | @Component({ 8 | selector: 'app-customers-grid', 9 | templateUrl: './customers-grid.component.html', 10 | //When using OnPush detectors, then the framework will check an OnPush 11 | //component when any of its input properties changes, when it fires 12 | //an event, or when an observable fires an event ~ Victor Savkin (Angular Team) 13 | changeDetection: ChangeDetectionStrategy.OnPush 14 | }) 15 | export class CustomersGridComponent implements OnInit { 16 | 17 | @Input() customers: ICustomer[] = []; 18 | 19 | constructor(private sorter: Sorter, public trackby: TrackByService) { } 20 | 21 | ngOnInit() { 22 | 23 | } 24 | 25 | sort(prop: string) { 26 | this.sorter.sort(this.customers, prop); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/app/customers/customers.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 6 | {{ title }} 7 |

8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | Add New Customer 18 |
19 |
20 | 21 | 22 | 23 | 26 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /public/runtime.f47bad7546ed9af5.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e,_={},p={};function n(e){var a=p[e];if(void 0!==a)return a.exports;var r=p[e]={exports:{}};return _[e](r,r.exports,n),r.exports}n.m=_,e=[],n.O=(a,r,u,t)=>{if(!r){var o=1/0;for(f=0;f=t)&&Object.keys(n.O).every(h=>n.O[h](r[l]))?r.splice(l--,1):(s=!1,t0&&e[f-1][2]>t;f--)e[f]=e[f-1];e[f]=[r,u,t]},n.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return n.d(a,{a}),a},n.d=(e,a)=>{for(var r in a)n.o(a,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:a[r]})},n.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),(()=>{var e={666:0};n.O.j=u=>0===e[u];var a=(u,t)=>{var l,c,[f,o,s]=t,v=0;if(f.some(d=>0!==e[d])){for(l in o)n.o(o,l)&&(n.m[l]=o[l]);if(s)var b=s(n)}for(u&&u(t);v { 10 | if (err) { 11 | console.log(`*** StatesRepository.getStates err: ${err}`); 12 | return callback(err); 13 | } 14 | callback(null, states); 15 | }); 16 | } 17 | 18 | // get a state 19 | getState(stateId, callback) { 20 | console.log('*** StatesRepository.getState'); 21 | State.find({ 'id': stateId }, {}, (err, state) => { 22 | if (err) { 23 | console.log(`*** StatesRepository.getState err: ${err}`); 24 | return callback(err); 25 | } 26 | callback(null, state); 27 | }); 28 | } 29 | } 30 | 31 | module.exports = new StatesRepository(); 32 | 33 | -------------------------------------------------------------------------------- /models/customer.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | State = require('./state'); 4 | 5 | //console.log(State); 6 | const OrderSchema = new Schema({ 7 | product : { type : String, required: true, trim: true }, 8 | price : { type : Number }, 9 | quantity : { type : Number } 10 | }); 11 | 12 | const CustomerSchema = new Schema({ 13 | firstName : { type : String, required: true, trim: true }, 14 | lastName : { type : String, required: true, trim: true }, 15 | email : { type : String, required: true, trim: true }, 16 | address : { type : String, required: true, trim: true }, 17 | city : { type : String, required: true, trim: true }, 18 | stateId : { type : Number, required: true }, 19 | state : State.schema , 20 | zip : { type : Number, required: true }, 21 | gender : { type : String }, 22 | orderCount : { type : Number }, 23 | orders : [ OrderSchema ], 24 | }); 25 | 26 | module.exports = mongoose.model('Customer', CustomerSchema, 'customers'); 27 | -------------------------------------------------------------------------------- /.docker/node.development.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | LABEL author="Dan Wahlin" 4 | 5 | WORKDIR /var/www/angularnoderestfulservice 6 | 7 | RUN npm install nodemon -g 8 | 9 | EXPOSE 3000 10 | 11 | ENTRYPOINT ["nodemon", "server.js"] 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | # Build: docker build -f node.dockerfile -t danwahlin/node . 26 | 27 | # Option 1 28 | # Start MongoDB and Node (link Node to MongoDB container with legacy linking) 29 | 30 | # docker run -d --name mongodb mongo 31 | # docker run -d -p 3000:3000 --link mongodb --name nodeapp danwahlin/node 32 | 33 | # Option 2: Create a custom bridge network and add containers into it 34 | 35 | # docker network create --driver bridge isolated_network 36 | # docker run -d --net=isolated_network --name mongodb mongo 37 | # docker run -d --net=isolated_network --name nodeapp -p 3000:3000 danwahlin/node 38 | 39 | # Option 3: Use Docker Compose 40 | 41 | # docker-compose build 42 | # docker-compose up 43 | 44 | # Seed the database with sample database 45 | # Run: docker exec nodeapp node lib/dbSeeder.js -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { CustomersComponent } from './customers/customers.component'; 5 | import { CustomersGridComponent } from './customers/customers-grid.component'; 6 | import { CustomerEditComponent } from './customers/customer-edit.component'; 7 | import { CustomerEditReactiveComponent } from './customers/customer-edit-reactive.component'; 8 | 9 | const routes: Routes = [ 10 | { path: '', pathMatch: 'full', redirectTo: '/customers' }, 11 | { path: 'customers', component: CustomersComponent}, 12 | { path: 'customers/:id', component: CustomerEditComponent}, 13 | //{ path: 'customers/:id', component: CustomerEditReactiveComponent }, 14 | { path: '**', redirectTo: '/customers' } //catch any unfound routes and redirect to home page 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [ RouterModule.forRoot(routes) ], 19 | exports: [ RouterModule ] 20 | }) 21 | export class AppRoutingModule { 22 | static components = [ CustomersComponent, CustomerEditComponent, CustomerEditReactiveComponent, CustomersGridComponent ]; 23 | } 24 | -------------------------------------------------------------------------------- /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/my-angular'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 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 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts.httpmodule: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | 3 | //Using the newer HttpClientModule now. 4 | //This is the pre-Angular 4.3 Http option. If you're not on Angular 4.3 yet, 5 | //simplify rename this file to core.module.ts to use it instead. 6 | import { HttpModule, XSRFStrategy, CookieXSRFStrategy } from '@angular/http'; 7 | 8 | import { DataService } from './data.service'; 9 | import { DataFilterService } from './data-filter.service'; 10 | import { Sorter } from './sorter'; 11 | import { TrackByService } from './trackby.service'; 12 | import { EnsureModuleLoadedOnceGuard } from '../shared/ensureModuleLoadedOnceGuard'; 13 | 14 | @NgModule({ 15 | imports: [ HttpModule ], 16 | providers: [ 17 | //Default XSRF provider setup (change cookie or header name if needed): 18 | //{ provide: XSRFStrategy, useValue: new CookieXSRFStrategy('XSRF-TOKEN', 'X-XSRF-TOKEN') }, 19 | DataService, DataFilterService, Sorter, TrackByService] // these should be singleton 20 | }) 21 | export class CoreModule extends EnsureModuleLoadedOnceGuard { //Ensure that CoreModule is only loaded into AppModule 22 | 23 | //Looks for the module in the parent injector to see if it's already been loaded (only want it loaded once) 24 | constructor( @Optional() @SkipSelf() parentModule: CoreModule) { 25 | super(parentModule); 26 | } 27 | 28 | } 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | // Module dependencies 2 | const mongoose = require('mongoose'), 3 | dbConfig = require('./configLoader').databaseConfig, 4 | connectionString = 'mongodb://' + dbConfig.host + '/' + dbConfig.database; 5 | 6 | let connection = null; 7 | 8 | class Database { 9 | 10 | open(callback) { 11 | var options = { 12 | promiseLibrary: global.Promise, 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true 15 | }; 16 | mongoose.connect(connectionString, options, (err) => { 17 | if (err) { 18 | console.log('mongoose.connect() failed: ' + err); 19 | } 20 | }); 21 | connection = mongoose.connection; 22 | 23 | mongoose.connection.on('error', (err) => { 24 | console.log('Error connecting to MongoDB: ' + err); 25 | callback(err, false); 26 | }); 27 | 28 | mongoose.connection.once('open', () => { 29 | console.log('We have connected to mongodb'); 30 | callback(null, true); 31 | }); 32 | 33 | } 34 | 35 | // disconnect from database 36 | close() { 37 | connection.close(() => { 38 | console.log('Mongoose default connection disconnected through app termination'); 39 | process.exit(0); 40 | }); 41 | } 42 | 43 | } 44 | 45 | module.exports = new Database(); 46 | -------------------------------------------------------------------------------- /.docker/useful-commands.md: -------------------------------------------------------------------------------- 1 | # Useful Docker Commands 2 | 3 | 4 | ## Docker Machine 5 | 6 | - `docker-machine start` - Start VM 7 | - `docker-machine stop` - Stop VM 8 | - `docker-machine env` - Display Docker client setup commands 9 | 10 | ## Docker Client 11 | 12 | - `docker --help` - Get help on a specific command 13 | - `docker pull ` - Pull image from Docker Hub 14 | - `docker images` - Show all images 15 | - `docker rmi ` - Remove specific images 16 | - `docker rmi $(docker images | grep "^" | awk "{print $3}")` - Remove untagged images - 17 | - `docker ps -a` - Show all containers 18 | - `docker rm ` -Remove specific container 19 | - `docker rm $(docker ps -a -q)` Remove all containers 20 | - `docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'` - Formatted list of containers 21 | - `docker run -d --name -p : ` - Run a container 22 | - `docker build -f -t .` - Build an image from a Dockerfile 23 | - `docker login` - Login using your Docker credentials 24 | - `docker push ` - Push an image to Docker hub 25 | 26 | ## Docker Compose 27 | 28 | - `docker-compose build` - Build images based on docker-compose 29 | - `docker-compose up -d` - Start in daemon mode 30 | - `docker-compose logs` - Show logs from containers 31 | - `docker-compose up` - Start containers based on docker-compose 32 | - `docker-compose stop` - Stop containers 33 | - `docker-compose down` - Stop and remove containers 34 | 35 | -------------------------------------------------------------------------------- /controllers/api/tokens/tokens.controller.js: -------------------------------------------------------------------------------- 1 | const util = require('util'), 2 | url = require('url'); 3 | 4 | //#### WARNING: Shown for an example but not recommended!!!! 5 | //#### Read more at https://github.com/pillarjs/understanding-csrf 6 | //#### The following is not recommended - said that twice now!!!! :-) 7 | 8 | class TokensController { 9 | 10 | constructor(router) { 11 | //Check referer 12 | router.use(this.refererCheck.bind(this)); 13 | 14 | //This can be VERY, VERY DANGEROUS if not done properly so just avoid it! Make sure: 15 | //1. CORS is disabled for this route if you've enabled CORS (CORS is not enabled in this app) 16 | // Note that disabling CORS won't prevent GET/POST requests using standard HTML though 17 | //2. Should always check referrer to be safe (see referrerCheck() middleware above) 18 | router.get('/csrf', this.getCsrfToken.bind(this)); 19 | } 20 | 21 | refererCheck(req, res, next) { 22 | //Simple check to ensure that calls to routes here are only supported for http(s)://localhost:3000 23 | var referer = url.parse(req.headers.referer); 24 | console.log('Referer: ' + req.headers.referer); 25 | if (referer.host !== 'localhost' && referer.port !== '3000') { 26 | throw new Error('Invalid request'); 27 | } 28 | next(); 29 | } 30 | 31 | getCsrfToken(req, res) { 32 | console.log('*** getCsrfToken'); 33 | res.json({ csrfToken: res.locals._csrf }); 34 | } 35 | } 36 | 37 | module.exports = TokensController; -------------------------------------------------------------------------------- /src/app/core/data-filter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { propertyResolver } from '../shared/property-resolver'; 3 | 4 | @Injectable() 5 | export class DataFilterService { 6 | 7 | filter(datasource: any[], filterProperties: string[], filterData: string) { 8 | if (datasource && filterProperties && filterData) { 9 | filterData = filterData.toUpperCase(); 10 | const filtered = datasource.filter(item => { 11 | let match = false; 12 | for (const prop of filterProperties) { 13 | let propVal: any = ''; 14 | 15 | //Account for nested properties like 'state.name' 16 | if (prop.indexOf('.') > -1) { 17 | propVal = propertyResolver.resolve(prop, item); 18 | if (propVal) { 19 | propVal = propVal.toString().toUpperCase(); 20 | } 21 | } 22 | else { 23 | if (item[prop]) { 24 | propVal = item[prop].toString().toUpperCase(); 25 | } 26 | } 27 | 28 | if (propVal.indexOf(filterData) > -1) { 29 | match = true; 30 | break; 31 | } 32 | }; 33 | return match; 34 | }); 35 | return filtered; 36 | } 37 | else { 38 | return datasource; 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/app/core/sorter.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { propertyResolver } from '../shared/property-resolver'; 3 | 4 | @Injectable() 5 | export class Sorter { 6 | 7 | property: string = null; 8 | direction: number = 1; 9 | 10 | sort(collection: any[], prop: any) { 11 | this.property = prop; 12 | this.direction = (this.property === prop) ? this.direction * -1 : 1; 13 | 14 | collection.sort((a: any,b: any) => { 15 | let aVal: any; 16 | let bVal: any; 17 | 18 | //Handle resolving complex properties such as 'state.name' for prop value 19 | if (prop && prop.indexOf('.') > -1) { 20 | aVal = propertyResolver.resolve(prop, a); 21 | bVal = propertyResolver.resolve(prop, b); 22 | } 23 | else { 24 | aVal = a[prop]; 25 | bVal = b[prop]; 26 | } 27 | 28 | //Fix issues that spaces before/after string value can cause such as ' San Francisco' 29 | if (this.isString(aVal)) aVal = aVal.trim().toUpperCase(); 30 | if (this.isString(bVal)) bVal = bVal.trim().toUpperCase(); 31 | 32 | if(aVal === bVal){ 33 | return 0; 34 | } 35 | else if (aVal > bVal){ 36 | return this.direction * -1; 37 | } 38 | else { 39 | return this.direction * 1; 40 | } 41 | }); 42 | } 43 | 44 | isString(val: any) : boolean { 45 | return (val && (typeof val === 'string' || val instanceof String)); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cli-node-js-mongo-db-customers-service", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "server": "nodemon server.js", 9 | "build": "ng build --prod", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~13.2.0", 17 | "@angular/common": "~13.2.0", 18 | "@angular/compiler": "~13.2.0", 19 | "@angular/core": "~13.2.0", 20 | "@angular/forms": "~13.2.0", 21 | "@angular/platform-browser": "~13.2.0", 22 | "@angular/platform-browser-dynamic": "~13.2.0", 23 | "@angular/router": "~13.2.0", 24 | "rxjs": "~7.5.0", 25 | "tslib": "^2.3.0", 26 | "zone.js": "~0.11.4", 27 | "cookie-parser": "^1.4.6", 28 | "body-parser": "^1.20.1", 29 | "csurf": "^1.11.0", 30 | "errorhandler": "^1.5.1", 31 | "express": "^4.18.2", 32 | "express-handlebars": "^5.3.1", 33 | "handlebars": "^4.7.7", 34 | "handlebars-helpers": "^0.10.0", 35 | "handlebars-layouts": "^3.1.4", 36 | "express-session": "^1.17.2", 37 | "hbs": "~4.2.0", 38 | "mongoose": "^6.4.6", 39 | "morgan": "~1.10.0", 40 | "serve-favicon": "~2.5.0" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "~13.3.9", 44 | "@angular/cli": "~13.2.3", 45 | "@angular/compiler-cli": "~13.2.0", 46 | "@types/jasmine": "~3.10.0", 47 | "@types/node": "^12.11.1", 48 | "jasmine-core": "~4.0.0", 49 | "karma": "~6.3.0", 50 | "karma-chrome-launcher": "~3.1.0", 51 | "karma-coverage": "~2.1.0", 52 | "karma-jasmine": "~4.0.0", 53 | "karma-jasmine-html-reporter": "~1.7.0", 54 | "typescript": "~4.5.2" 55 | } 56 | } -------------------------------------------------------------------------------- /routes/router.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'), 3 | express = require('express'); 4 | 5 | class Router { 6 | 7 | constructor() { 8 | this.startFolder = null; 9 | } 10 | 11 | //Called once during initial server startup 12 | load(app, folderName) { 13 | 14 | if (!this.startFolder) this.startFolder = path.basename(folderName); 15 | 16 | fs.readdirSync(folderName).forEach((file) => { 17 | 18 | const fullName = path.join(folderName, file); 19 | const stat = fs.lstatSync(fullName); 20 | 21 | if (stat.isDirectory()) { 22 | //Recursively walk-through folders 23 | this.load(app, fullName); 24 | } else if (file.toLowerCase().indexOf('.js')) { 25 | //Grab path to JavaScript file and use it to construct the route 26 | let dirs = path.dirname(fullName).split(path.sep); 27 | 28 | if (dirs[0].toLowerCase() === this.startFolder.toLowerCase()) { 29 | dirs.splice(0, 1); 30 | } 31 | 32 | const router = express.Router(); 33 | //Generate the route 34 | const baseRoute = '/' + dirs.join('/'); 35 | console.log('Created route: ' + baseRoute + ' for ' + fullName); 36 | 37 | //Load the JavaScript file ("controller") and pass the router to it 38 | const controllerClass = require('../' + fullName); 39 | const controller = new controllerClass(router); 40 | //Associate the route with the router 41 | app.use(baseRoute, router); 42 | } 43 | }); 44 | } 45 | 46 | } 47 | 48 | module.exports = new Router(); 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/app/customers/customers.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { DataFilterService } from '../core/data-filter.service'; 5 | import { DataService } from '../core/data.service'; 6 | import { ICustomer, IOrder, IPagedResults } from '../shared/interfaces'; 7 | 8 | @Component({ 9 | selector: 'app-customers', 10 | templateUrl: './customers.component.html' 11 | }) 12 | export class CustomersComponent implements OnInit { 13 | 14 | title: string; 15 | customers: ICustomer[] = []; 16 | filteredCustomers: ICustomer[] = []; 17 | 18 | totalRecords: number = 0; 19 | pageSize: number = 10; 20 | 21 | constructor(private router: Router, 22 | private dataService: DataService, 23 | private dataFilter: DataFilterService) { } 24 | 25 | ngOnInit() { 26 | this.title = 'Customers'; 27 | this.getCustomersPage(1); 28 | } 29 | 30 | filterChanged(filterText: string) { 31 | if (filterText && this.customers) { 32 | let props = ['firstName', 'lastName', 'address', 'city', 'state.name', 'orderTotal']; 33 | this.filteredCustomers = this.dataFilter.filter(this.customers, props, filterText); 34 | } 35 | else { 36 | this.filteredCustomers = this.customers; 37 | } 38 | } 39 | 40 | pageChanged(page: number) { 41 | this.getCustomersPage(page); 42 | } 43 | 44 | getCustomersPage(page: number) { 45 | this.dataService.getCustomersPage((page - 1) * this.pageSize, this.pageSize) 46 | .subscribe((response: IPagedResults) => { 47 | this.customers = this.filteredCustomers = response.results; 48 | this.totalRecords = response.totalRecords; 49 | }, 50 | (err: any) => console.log(err), 51 | () => console.log('getCustomersPage() retrieved customers')); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/app/shared/validation.service.ts: -------------------------------------------------------------------------------- 1 | //Original version created by Cory Rylan: https://coryrylan.com/blog/angular-2-form-builder-and-validation-management 2 | import { AbstractControl } from '@angular/forms'; 3 | 4 | export class ValidationService { 5 | 6 | static getValidatorErrorMessage(code: string) { 7 | let config = { 8 | 'required': 'Required', 9 | 'invalidCreditCard': 'Is invalid credit card number', 10 | 'invalidEmailAddress': 'Invalid email address', 11 | 'invalidPassword': 'Invalid password. Password must be at least 6 characters long, and contain a number.' 12 | }; 13 | return config[code]; 14 | } 15 | 16 | static creditCardValidator(control: AbstractControl) { 17 | // Visa, MasterCard, American Express, Diners Club, Discover, JCB 18 | if (control.value.match(/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/)) { 19 | return null; 20 | } else { 21 | return { 'invalidCreditCard': true }; 22 | } 23 | } 24 | 25 | static emailValidator(control: AbstractControl) { 26 | // RFC 2822 compliant regex 27 | if (control.value.match(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/)) { 28 | return null; 29 | } else { 30 | return { 'invalidEmailAddress': true }; 31 | } 32 | } 33 | 34 | static passwordValidator(control: AbstractControl) { 35 | // {6,100} - Assert password is between 6 and 100 characters 36 | // (?=.*[0-9]) - Assert a string has at least one number 37 | if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!@#$%^&*]{6,100}$/)) { 38 | return null; 39 | } else { 40 | return { 'invalidPassword': true }; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/app/customers/customers-grid.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
 First NameLast NameAddressCityStateOrder Total
Customer Image{{ customer.firstName | capitalize }}{{ customer.lastName | capitalize }}{{ customer.address }}{{ customer.city | trim }}{{ customer.state.name }}{{ customer.orderTotal | currency:'USD':'symbol' }}
 No Records Found
34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular TypeScript App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 29 | 30 |
31 | 32 | Loading... 33 | 34 |

35 |
36 | 37 |
38 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/app/shared/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-pagination', 5 | templateUrl: './pagination.component.html', 6 | styleUrls: [ './pagination.component.css' ] 7 | }) 8 | 9 | export class PaginationComponent implements OnInit { 10 | 11 | private pagerTotalItems: number; 12 | private pagerPageSize: number; 13 | 14 | totalPages: number; 15 | pages: number[] = []; 16 | currentPage: number = 1; 17 | isVisible: boolean = false; 18 | previousEnabled: boolean = false; 19 | nextEnabled: boolean = true; 20 | 21 | @Input() get pageSize():number { 22 | return this.pagerPageSize; 23 | } 24 | 25 | set pageSize(size:number) { 26 | this.pagerPageSize = size; 27 | this.update(); 28 | } 29 | 30 | @Input() get totalItems():number { 31 | return this.pagerTotalItems; 32 | } 33 | 34 | set totalItems(itemCount:number) { 35 | this.pagerTotalItems = itemCount; 36 | this.update(); 37 | } 38 | 39 | @Output() pageChanged: EventEmitter = new EventEmitter(); 40 | 41 | constructor() { } 42 | 43 | ngOnInit() { 44 | 45 | } 46 | 47 | update() { 48 | if (this.pagerTotalItems && this.pagerPageSize) { 49 | this.totalPages = Math.ceil(this.pagerTotalItems/this.pageSize); 50 | this.isVisible = true; 51 | if (this.totalItems >= this.pageSize) { 52 | for (let i = 1;i < this.totalPages + 1;i++) { 53 | this.pages.push(i); 54 | } 55 | } 56 | return; 57 | } 58 | 59 | this.isVisible = false; 60 | } 61 | 62 | previousNext(direction: number, event?: MouseEvent) { 63 | let page: number = this.currentPage; 64 | if (direction == -1) { 65 | if (page > 1) page--; 66 | } else { 67 | if (page < this.totalPages) page++; 68 | } 69 | this.changePage(page, event); 70 | } 71 | 72 | changePage(page: number, event?: MouseEvent) { 73 | if (event) { 74 | event.preventDefault(); 75 | } 76 | if (this.currentPage === page) return; 77 | this.currentPage = page; 78 | this.previousEnabled = this.currentPage > 1; 79 | this.nextEnabled = this.currentPage < this.totalPages; 80 | this.pageChanged.emit(page); 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | 3 | //Using the new HttpClientModule now. If you're still on < Angular 4.3 see the 4 | //core.module.ts.httpmodule file instead (simply rename it to the name 5 | //of this file to use it instead) 6 | import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'; 7 | 8 | import { DataService } from './data.service'; 9 | import { DataFilterService } from './data-filter.service'; 10 | import { Sorter } from './sorter'; 11 | import { TrackByService } from './trackby.service'; 12 | import { EnsureModuleLoadedOnceGuard } from '../shared/ensureModuleLoadedOnceGuard'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | //Can use with Angular 4.3+ to 17 | HttpClientModule, 18 | //Can use to override default names for XSRF cookie and header 19 | // HttpClientXsrfModule.withOptions({ 20 | // cookieName: 'My-XSRF-TOKEN', 21 | // headerName: 'My-X-XSRF-TOKEN', 22 | // }) 23 | ], 24 | providers: [ DataService, DataFilterService, Sorter, TrackByService] 25 | }) 26 | export class CoreModule extends EnsureModuleLoadedOnceGuard { //Ensure that CoreModule is only loaded into AppModule 27 | 28 | //Looks for the module in the parent injector to see if it's already been loaded (only want it loaded once) 29 | constructor( @Optional() @SkipSelf() parentModule: CoreModule) { 30 | super(parentModule); 31 | } 32 | 33 | } 34 | 35 | //Example of a custom XSRF class 36 | //export class MyCookieXSRFStrategy implements XSRFStrategy { 37 | // constructor( 38 | // private _cookieName: string = 'XSRF-TOKEN', private _headerName: string = 'X-XSRF-TOKEN') { } 39 | 40 | // private getCookie(name: string) { 41 | // let ca: Array = document.cookie.split(';'); 42 | // let caLen: number = ca.length; 43 | // let cookieName = name + "="; 44 | // let c: string; 45 | 46 | // for (let i: number = 0; i < caLen; i += 1) { 47 | // c = ca[i].replace(/^\s\+/g, ""); 48 | // if (c.indexOf(cookieName) == 0) { 49 | // return c.substring(cookieName.length, c.length); 50 | // } 51 | // } 52 | // return ""; 53 | // } 54 | 55 | // configureRequest(req: Request) { 56 | // let xsrfToken = this.getCookie(this._cookieName); 57 | // alert(xsrfToken); 58 | // if (xsrfToken) { 59 | // req.headers.set(this._headerName, xsrfToken); 60 | // } 61 | // } 62 | //} 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular, NodeJS, MongoDB Customers Service 2 | 3 | This project provides a look at getting started using Angular Http functionality and how it can be used 4 | to call a Node.js RESTful service. The code is for the Integrating Angular with Node.js RESTful Services 5 | available on Pluralsight at https://www.pluralsight.com/courses/angular-nodejs-restful-services. 6 | 7 | ## Angular Concepts Covered 8 | 9 | * Using the Angular CLI 10 | * Using TypeScript classes and modules 11 | * Using Custom Components 12 | * Using the Http object for Ajax calls along with RxJS observables 13 | * Performing GET, PUT, POST and DELETE requests to the server 14 | * Working with Angular service classes for Ajax calls 15 | * Using Angular databinding and built-in directives 16 | * Creating a RESTful Service using Node.js 17 | * (Optional) Using Docker containers 18 | 19 | ## Software Requirements To Run Locally (there's a Docker option below as well) 20 | 21 | * Node.js 10.16 or higher 22 | * MongoDB 3.4 or higher 23 | 24 | ### Running the Application Locally 25 | 26 | 1. Install Node.js (14 or higher) and MongoDB (3.4 or higher) on your dev box 27 | 28 | * Node.js: https://nodejs.org 29 | * MongoDB: https://docs.mongodb.com/manual/administration/install-community 30 | 31 | 1. Execute `mongod` to start the MongoDB daemon if it's not already running (read the installation instructions above if you are new to MongoDB or have issues running it) 32 | 33 | 1. Run `npm install -g @angular/cli nodemon` to install the Angular CLI and nodemon. 34 | 35 | 1. Run `npm install` at the project root to install the app dependencies 36 | 37 | 1. Run the following task to build the Angular app (and watch for any changes you make) and copy the built code to the `public` folder: 38 | 39 | `ng build --watch` 40 | 41 | 1. Run `npm run server` in another console window to start the Node.js server 42 | 43 | 1. Browse to http://localhost:3000 44 | 45 | 46 | ## Running the Application with Docker 47 | 48 | 1. Install Node.js (14 or higher) and Docker for Mac/Windows or Docker Toolbox - https://www.docker.com/products/overview 49 | 50 | 1. Open `config/config.development.json` and change the host from `localhost` to `mongodb` 51 | 52 | 1. Run `npm install` 53 | 54 | 1. Run `ng build --watch` 55 | 56 | 1. Open another command window and navigate to this application's root folder in the command window 57 | 58 | 1. Run `docker-compose build` to build the images 59 | 60 | 1. Run `docker-compose up` to run the containers 61 | 62 | 1. Navigate to http://localhost:3000 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/app/customers/customer-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { DataService } from '../core/data.service'; 5 | import { ICustomer, IState } from '../shared/interfaces'; 6 | 7 | @Component({ 8 | selector: 'app-customer-edit', 9 | templateUrl: './customer-edit.component.html' 10 | }) 11 | export class CustomerEditComponent implements OnInit { 12 | 13 | customer: ICustomer = { 14 | firstName: '', 15 | lastName: '', 16 | gender: '', 17 | address: '', 18 | email: '', 19 | city: '', 20 | zip: 0 21 | }; 22 | states: IState[]; 23 | errorMessage: string; 24 | deleteMessageEnabled: boolean; 25 | operationText: string = 'Insert'; 26 | 27 | constructor(private router: Router, 28 | private route: ActivatedRoute, 29 | private dataService: DataService) { } 30 | 31 | ngOnInit() { 32 | let id = this.route.snapshot.params['id']; 33 | if (id !== '0') { 34 | this.operationText = 'Update'; 35 | this.getCustomer(id); 36 | } 37 | 38 | this.getStates(); 39 | } 40 | 41 | getCustomer(id: string) { 42 | this.dataService.getCustomer(id) 43 | .subscribe((customer: ICustomer) => { 44 | this.customer = customer; 45 | }, 46 | (err: any) => console.log(err)); 47 | } 48 | 49 | getStates() { 50 | this.dataService.getStates().subscribe((states: IState[]) => this.states = states); 51 | } 52 | 53 | submit() { 54 | 55 | if (this.customer._id) { 56 | 57 | this.dataService.updateCustomer(this.customer) 58 | .subscribe((customer: ICustomer) => { 59 | if (customer) { 60 | this.router.navigate(['/customers']); 61 | } else { 62 | this.errorMessage = 'Unable to save customer'; 63 | } 64 | }, 65 | (err: any) => console.log(err)); 66 | 67 | } else { 68 | 69 | this.dataService.insertCustomer(this.customer) 70 | .subscribe((customer: ICustomer) => { 71 | if (customer) { 72 | this.router.navigate(['/customers']); 73 | } 74 | else { 75 | this.errorMessage = 'Unable to add customer'; 76 | } 77 | }, 78 | (err: any) => console.log(err)); 79 | 80 | } 81 | } 82 | 83 | cancel(event: Event) { 84 | event.preventDefault(); 85 | this.router.navigate(['/']); 86 | } 87 | 88 | delete(event: Event) { 89 | event.preventDefault(); 90 | this.dataService.deleteCustomer(this.customer._id) 91 | .subscribe((status: boolean) => { 92 | if (status) { 93 | this.router.navigate(['/customers']); 94 | } 95 | else { 96 | this.errorMessage = 'Unable to delete customer'; 97 | } 98 | }, 99 | (err) => console.log(err)); 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "my-project": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:application": { 10 | "strict": true 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "public", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.css" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "budgets": [ 37 | { 38 | "type": "initial", 39 | "maximumWarning": "500kb", 40 | "maximumError": "1mb" 41 | }, 42 | { 43 | "type": "anyComponentStyle", 44 | "maximumWarning": "2kb", 45 | "maximumError": "4kb" 46 | } 47 | ], 48 | "fileReplacements": [ 49 | { 50 | "replace": "src/environments/environment.ts", 51 | "with": "src/environments/environment.prod.ts" 52 | } 53 | ], 54 | "outputHashing": "all" 55 | }, 56 | "development": { 57 | "buildOptimizer": false, 58 | "optimization": false, 59 | "vendorChunk": true, 60 | "extractLicenses": false, 61 | "sourceMap": true, 62 | "namedChunks": true 63 | } 64 | }, 65 | "defaultConfiguration": "production" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "configurations": { 70 | "production": { 71 | "browserTarget": "my-project:build:production" 72 | }, 73 | "development": { 74 | "browserTarget": "my-project:build:development" 75 | } 76 | }, 77 | "defaultConfiguration": "development" 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "my-project:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "src/styles.css" 98 | ], 99 | "scripts": [] 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "defaultProject": "angular-cli-node-js-mongo-db-customers-service" 106 | } -------------------------------------------------------------------------------- /src/app/customers/customer-edit-reactive.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | {{ customer.firstName }} {{ customer.lastName }} 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 |
First Name is required
14 |
15 |
16 | 17 | 18 |
Last Name is required
19 |
20 |
21 | 22 |
23 |
24 | 28 |
29 |
30 | 34 |
35 |
36 |
37 | 38 | 39 |
Email is required and must be valid
40 |
41 |
42 | 43 | 44 |
Address is required
45 |
46 |
47 | 48 | 49 |
City is required
50 |
51 |
52 | 53 | 56 |
57 |
58 | 59 |
60 |
61 | Delete Customer?     62 | 63 |
64 |    65 | 66 |
67 |    68 | 69 |
70 |
71 |
72 |
73 |
{{ errorMessage }}
74 | 75 |
76 |
-------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | exphbs = require('express-handlebars'), 3 | hbsHelpers = require('handlebars-helpers'), 4 | hbsLayouts = require('handlebars-layouts'), 5 | bodyParser = require('body-parser'), 6 | cookieParser = require('cookie-parser'), 7 | errorhandler = require('errorhandler'), 8 | csrf = require('csurf'), 9 | morgan = require('morgan'), 10 | favicon = require('serve-favicon'), 11 | 12 | router = require('./routes/router'), 13 | database = require('./lib/database'), 14 | seeder = require('./lib/dbSeeder'), 15 | app = express(), 16 | port = 3000; 17 | 18 | class Server { 19 | 20 | constructor() { 21 | this.initViewEngine(); 22 | this.initExpressMiddleWare(); 23 | this.initCustomMiddleware(); 24 | this.initDbSeeder(); 25 | this.initRoutes(); 26 | this.start(); 27 | } 28 | 29 | start() { 30 | app.listen(port, (err) => { 31 | console.log('[%s] Listening on http://localhost:%d', process.env.NODE_ENV, port); 32 | }); 33 | } 34 | 35 | initViewEngine() { 36 | const hbs = exphbs.create({ 37 | extname: '.hbs', 38 | defaultLayout: 'master' 39 | }); 40 | app.engine('hbs', hbs.engine); 41 | app.set('view engine', 'hbs'); 42 | hbsLayouts.register(hbs.handlebars, {}); 43 | } 44 | 45 | initExpressMiddleWare() { 46 | app.use(favicon(__dirname + '/public/assets/images/favicon.ico')); 47 | app.use(express.static(__dirname + '/public')); 48 | app.use(morgan('dev')); 49 | app.use(bodyParser.urlencoded({ extended: true })); 50 | app.use(bodyParser.json()); 51 | app.use(errorhandler()); 52 | app.use(cookieParser()); 53 | app.use(csrf({ cookie: true })); 54 | 55 | app.use((req, res, next) => { 56 | let csrfToken = req.csrfToken(); 57 | res.locals._csrf = csrfToken; 58 | res.cookie('XSRF-TOKEN', csrfToken); 59 | next(); 60 | }); 61 | 62 | process.on('uncaughtException', (err) => { 63 | if (err) console.log(err, err.stack); 64 | }); 65 | } 66 | 67 | initCustomMiddleware() { 68 | if (process.platform === "win32") { 69 | require("readline").createInterface({ 70 | input: process.stdin, 71 | output: process.stdout 72 | }).on("SIGINT", () => { 73 | console.log('SIGINT: Closing MongoDB connection'); 74 | database.close(); 75 | }); 76 | } 77 | 78 | process.on('SIGINT', () => { 79 | console.log('SIGINT: Closing MongoDB connection'); 80 | database.close(); 81 | }); 82 | } 83 | 84 | initDbSeeder() { 85 | database.open(() => { 86 | //Set NODE_ENV to 'development' and uncomment the following if to only run 87 | //the seeder when in dev mode 88 | //if (process.env.NODE_ENV === 'development') { 89 | // seeder.init(); 90 | //} 91 | seeder.init(); 92 | }); 93 | } 94 | 95 | initRoutes() { 96 | router.load(app, './controllers'); 97 | 98 | // redirect all others to the index (HTML5 history) 99 | app.all('/*', (req, res) => { 100 | res.sendFile(__dirname + '/public/index.html'); 101 | }); 102 | } 103 | 104 | } 105 | 106 | let server = new Server(); -------------------------------------------------------------------------------- /src/app/customers/customer-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | {{ customer.firstName }} {{ customer.lastName }} 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 |
First Name is required
14 |
15 |
16 | 17 | 18 |
Last Name is required
19 |
20 |
21 | 22 |
23 |
24 | 28 |
29 |
30 | 34 |
35 |
36 |
37 | 38 | 39 |
Email is required and must be valid
40 |
41 |
42 | 43 | 44 |
Address is required
45 |
46 |
47 | 48 | 49 |
City is required
50 |
51 |
52 | 53 | 59 |
60 |
61 | 62 |
63 |
64 | Delete Customer?     65 | 66 |
67 |    68 | 69 |
70 |    71 | 72 |
73 |
74 |
75 |
76 |
{{ errorMessage }}
77 | 78 |
79 |
-------------------------------------------------------------------------------- /src/app/customers/customer-edit-reactive.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | 5 | import { DataService } from '../core/data.service'; 6 | import { ICustomer, IState } from '../shared/interfaces'; 7 | import { ValidationService } from '../shared/validation.service'; 8 | 9 | @Component({ 10 | selector: 'app-customer-edit-reactive', 11 | templateUrl: './customer-edit-reactive.component.html' 12 | }) 13 | export class CustomerEditReactiveComponent implements OnInit { 14 | 15 | customerForm: FormGroup; 16 | customer: ICustomer = { 17 | firstName: '', 18 | lastName: '', 19 | gender: '', 20 | address: '', 21 | email: '', 22 | city: '', 23 | zip: 0 24 | }; 25 | states: IState[]; 26 | errorMessage: string; 27 | deleteMessageEnabled: boolean; 28 | operationText: string = 'Insert'; 29 | 30 | constructor(private router: Router, 31 | private route: ActivatedRoute, 32 | private dataService: DataService, 33 | private formBuilder: FormBuilder) { } 34 | 35 | ngOnInit() { 36 | let id = this.route.snapshot.params['id']; 37 | if (id !== '0') { 38 | this.operationText = 'Update'; 39 | this.getCustomer(id); 40 | } 41 | 42 | this.getStates(); 43 | this.buildForm(); 44 | } 45 | 46 | getCustomer(id: string) { 47 | this.dataService.getCustomer(id) 48 | .subscribe((customer: ICustomer) => { 49 | this.customer = customer; 50 | this.buildForm(); 51 | }, 52 | (err) => console.log(err)); 53 | } 54 | 55 | buildForm() { 56 | this.customerForm = this.formBuilder.group({ 57 | firstName: [this.customer.firstName, Validators.required], 58 | lastName: [this.customer.lastName, Validators.required], 59 | gender: [this.customer.gender, Validators.required], 60 | email: [this.customer.email, [Validators.required, ValidationService.emailValidator]], 61 | address: [this.customer.address, Validators.required], 62 | city: [this.customer.city, Validators.required], 63 | stateId: [this.customer.stateId, Validators.required] 64 | }); 65 | } 66 | 67 | getStates() { 68 | this.dataService.getStates().subscribe((states: IState[]) => this.states = states); 69 | } 70 | 71 | submit({ value, valid }: { value: ICustomer, valid: boolean }) { 72 | 73 | value._id = this.customer._id; 74 | value.zip = this.customer.zip || 0; 75 | // var customer: ICustomer = { 76 | // _id: this.customer._id, 77 | // }; 78 | 79 | if (value._id) { 80 | 81 | this.dataService.updateCustomer(value) 82 | .subscribe((customer: ICustomer) => { 83 | if (customer) { 84 | this.router.navigate(['/customers']); 85 | } 86 | else { 87 | this.errorMessage = 'Unable to save customer'; 88 | } 89 | }, 90 | (err) => console.log(err)); 91 | 92 | } else { 93 | 94 | this.dataService.insertCustomer(value) 95 | .subscribe((customer: ICustomer) => { 96 | if (customer) { 97 | this.router.navigate(['/customers']); 98 | } 99 | else { 100 | this.errorMessage = 'Unable to add customer'; 101 | } 102 | }, 103 | (err) => console.log(err)); 104 | 105 | } 106 | } 107 | 108 | cancel(event: Event) { 109 | event.preventDefault(); 110 | this.router.navigate(['/customers']); 111 | } 112 | 113 | delete(event: Event) { 114 | event.preventDefault(); 115 | this.dataService.deleteCustomer(this.customer._id) 116 | .subscribe((status: boolean) => { 117 | if (status) { 118 | this.router.navigate(['/customers']); 119 | } 120 | else { 121 | this.errorMessage = 'Unable to delete customer'; 122 | } 123 | }, 124 | (err) => console.log(err)); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/app/core/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | //Using the new HttpClientModule now. If you're still on < Angular 4.3 see the 4 | //data.service.ts file instead (simplify rename it to the name 5 | //of this file to use it instead) 6 | import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 7 | 8 | import { Observable, throwError } from 'rxjs'; 9 | import { map, catchError } from 'rxjs/operators'; 10 | 11 | import { ICustomer, IOrder, IState, IPagedResults, ICustomerResponse } from '../shared/interfaces'; 12 | 13 | @Injectable() 14 | export class DataService { 15 | 16 | baseUrl: string = '/api/customers'; 17 | baseStatesUrl: string = '/api/states' 18 | 19 | constructor(private http: HttpClient) { 20 | 21 | } 22 | 23 | getCustomers() : Observable { 24 | return this.http.get(this.baseUrl) 25 | .pipe( 26 | map((customers: ICustomer[]) => { 27 | this.calculateCustomersOrderTotal(customers); 28 | return customers; 29 | }), 30 | catchError(this.handleError) 31 | ); 32 | } 33 | 34 | getCustomersPage(page: number, pageSize: number) : Observable> { 35 | return this.http.get(`${this.baseUrl}/page/${page}/${pageSize}`, { observe: 'response' }) 36 | .pipe( 37 | map((res) => { 38 | //Need to observe response in order to get to this header (see {observe: 'response'} above) 39 | const totalRecords = +res.headers.get('x-inlinecount'); 40 | let customers = res.body as ICustomer[]; 41 | this.calculateCustomersOrderTotal(customers); 42 | return { 43 | results: customers, 44 | totalRecords: totalRecords 45 | }; 46 | }), 47 | catchError(this.handleError) 48 | ); 49 | } 50 | 51 | getCustomer(id: string) : Observable { 52 | return this.http.get(this.baseUrl + '/' + id) 53 | .pipe( 54 | catchError(this.handleError) 55 | ); 56 | } 57 | 58 | insertCustomer(customer: ICustomer) : Observable { 59 | return this.http.post(this.baseUrl, customer) 60 | .pipe( 61 | map((data) => { 62 | console.log('insertCustomer status: ' + data.status); 63 | return data.customer; 64 | }), 65 | catchError(this.handleError) 66 | ); 67 | } 68 | 69 | updateCustomer(customer: ICustomer) : Observable { 70 | return this.http.put(this.baseUrl + '/' + customer._id, customer) 71 | .pipe( 72 | map((data) => { 73 | console.log('updateCustomer status: ' + data.status); 74 | return data.customer; 75 | }), 76 | catchError(this.handleError) 77 | ); 78 | } 79 | 80 | deleteCustomer(id: string) : Observable { 81 | return this.http.delete(this.baseUrl + '/' + id) 82 | .pipe( 83 | catchError(this.handleError) 84 | ); 85 | } 86 | 87 | getStates(): Observable { 88 | return this.http.get(this.baseStatesUrl) 89 | .pipe( 90 | catchError(this.handleError) 91 | ); 92 | } 93 | 94 | calculateCustomersOrderTotal(customers: ICustomer[]) { 95 | for (let customer of customers) { 96 | if (customer && customer.orders) { 97 | let total = 0; 98 | for (let order of customer.orders) { 99 | total += (order.price * order.quantity); 100 | } 101 | customer.orderTotal = total; 102 | } 103 | } 104 | } 105 | 106 | private handleError(error: HttpErrorResponse) { 107 | console.error('server error:', error); 108 | if (error.error instanceof Error) { 109 | let errMessage = error.error.message; 110 | return throwError(() => new Error(errMessage)); 111 | // Use the following instead if using lite-server 112 | //return Observable.throw(err.text() || 'backend server error'); 113 | } 114 | return throwError(() => new Error(error.message || 'Node.js server error')); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /public/assets/styles/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow-y: scroll; 3 | overflow-x: hidden; 4 | } 5 | 6 | body { 7 | font-family: 'Open Sans' 8 | } 9 | 10 | main { 11 | position: relative; 12 | padding-top: 60px; 13 | } 14 | 15 | /* Ensure display:flex and others don't override a [hidden] */ 16 | [hidden] { display: none !important; } 17 | 18 | .white { 19 | color: white; 20 | } 21 | 22 | .white:hover{ 23 | color: white; 24 | } 25 | 26 | th { 27 | cursor: pointer; 28 | } 29 | 30 | .navbar-header { 31 | width: 300px; 32 | } 33 | 34 | .nav.navbar-padding { 35 | margin-left:25px; 36 | margin-top: 10px; 37 | } 38 | 39 | .app-title { 40 | line-height:50px; 41 | font-size:20px; 42 | color: white; 43 | } 44 | 45 | .navbar .nav > li.toolbar-item > a { 46 | color: #9E9E9E; 47 | font-weight:bold; 48 | -webkit-text-shadow: none; 49 | text-shadow: none; 50 | } 51 | 52 | .navbar .nav > .toolbar-item > a.active { 53 | color: #000; 54 | } 55 | 56 | .toolbar-item a { 57 | cursor: pointer; 58 | } 59 | 60 | .view { 61 | 62 | } 63 | 64 | .indent { 65 | margin-left:5px; 66 | } 67 | 68 | .card-container { 69 | width:85%; 70 | } 71 | 72 | .card { 73 | background-color:#fff; 74 | border: 1px solid #d4d4d4; 75 | height:100px; 76 | margin-bottom: 20px; 77 | position: relative; 78 | } 79 | 80 | .card-header { 81 | background-color:#027FF4; 82 | font-size:14pt; 83 | color:white; 84 | padding:5px; 85 | width:100%; 86 | } 87 | 88 | .card-close { 89 | color: white; 90 | font-weight:bold; 91 | margin-right:5px; 92 | } 93 | 94 | .card-body { 95 | padding-left: 5px; 96 | } 97 | 98 | .card-body-left { 99 | margin-top: -5px; 100 | } 101 | 102 | .card-body-right { 103 | margin-left: 20px; 104 | margin-top: 2px; 105 | } 106 | 107 | .card-body-content { 108 | width: 100px; 109 | } 110 | 111 | .card-image { 112 | height:50px;width:50px;margin-top:10px; 113 | } 114 | 115 | .grid-container div { 116 | padding-left: 0px; 117 | } 118 | 119 | .grid-container td { 120 | vertical-align: middle; 121 | } 122 | 123 | .navbar-brand { 124 | float:none; 125 | } 126 | 127 | a.navbar-brand { 128 | color: #fff; 129 | } 130 | 131 | .navbar-inner { 132 | padding-left: 0px; 133 | -webkit-border-radius: 0px; 134 | border-radius: 0px; 135 | -webkit-box-shadow: none; 136 | -moz-box-shadow: none; 137 | box-shadow: none; 138 | background-color: #027FF4; 139 | background-image: none; 140 | } 141 | 142 | .navbar-inner.toolbar { 143 | background-color: #fafafa; 144 | } 145 | 146 | footer { 147 | margin-top: 10px; 148 | } 149 | 150 | .navbar-inner.footer { 151 | background-color: #fafafa; 152 | -webkit-box-shadow: none; 153 | -moz-box-shadow: none; 154 | box-shadow: none; 155 | height:50px; 156 | } 157 | 158 | .navbar .nav > .active > a, .navbar .nav > .active > a:hover, .navbar .nav > .active > a:focus { 159 | background-color: #efefef; 160 | -webkit-box-shadow: none; 161 | box-shadow: none; 162 | color: #808080; 163 | } 164 | 165 | .navbar .nav li.toolbaritem a:hover, .navbar .nav li a:hover { 166 | color: #E03930; 167 | } 168 | 169 | .navbar .nav > li { 170 | cursor:pointer; 171 | } 172 | 173 | .navbar .nav > li > a { 174 | color: white; 175 | font-weight:bold; 176 | -webkit-text-shadow: none; 177 | text-shadow: none; 178 | height:30px; 179 | padding-top: 6px; 180 | padding-bottom: 0px; 181 | } 182 | 183 | .navbar .nav > li.toolbaritem > a { 184 | color: black; 185 | font-weight:bold; 186 | -webkit-text-shadow: none; 187 | text-shadow: none; 188 | } 189 | 190 | 191 | .navbar-fixed-top .navbar-inner, 192 | .navbar-static-top .navbar-inner { 193 | -webkit-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 194 | -moz-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 195 | box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 196 | } 197 | 198 | .nav.navBarPadding { 199 | margin-left:25px; 200 | margin-top: 10px; 201 | } 202 | 203 | .navbarText { 204 | font-weight:bold; 205 | } 206 | 207 | .navbar .brand { 208 | margin-top: 2px; 209 | color: #fff; 210 | -webkit-text-shadow: none; 211 | text-shadow: none; 212 | } 213 | 214 | .navbar-toggle { 215 | border: 1px solid white; 216 | } 217 | 218 | .navbar-toggle .icon-bar { 219 | background-color: white; 220 | } 221 | 222 | footer { 223 | margin-top: 15px; 224 | } 225 | 226 | .navbar-inner.footer { 227 | background-color: #fafafa; 228 | -webkit-box-shadow: none; 229 | -moz-box-shadow: none; 230 | box-shadow: none; 231 | height:50px; 232 | } 233 | 234 | filter-textbox { 235 | margin-top: 5px; 236 | } 237 | 238 | form { 239 | width:50%; 240 | } 241 | 242 | .editForm .ng-invalid { 243 | border-left: 5px solid #a94442; 244 | } 245 | 246 | .editForm .ng-valid { 247 | border-left: 5px solid #42A948; 248 | } 249 | 250 | -------------------------------------------------------------------------------- /src/assets/styles/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow-y: scroll; 3 | overflow-x: hidden; 4 | } 5 | 6 | body { 7 | font-family: 'Open Sans' 8 | } 9 | 10 | main { 11 | position: relative; 12 | padding-top: 60px; 13 | } 14 | 15 | /* Ensure display:flex and others don't override a [hidden] */ 16 | [hidden] { display: none !important; } 17 | 18 | .white { 19 | color: white; 20 | } 21 | 22 | .white:hover{ 23 | color: white; 24 | } 25 | 26 | th { 27 | cursor: pointer; 28 | } 29 | 30 | .navbar-header { 31 | width: 300px; 32 | } 33 | 34 | .nav.navbar-padding { 35 | margin-left:25px; 36 | margin-top: 10px; 37 | } 38 | 39 | .app-title { 40 | line-height:50px; 41 | font-size:20px; 42 | color: white; 43 | } 44 | 45 | .navbar .nav > li.toolbar-item > a { 46 | color: #9E9E9E; 47 | font-weight:bold; 48 | -webkit-text-shadow: none; 49 | text-shadow: none; 50 | } 51 | 52 | .navbar .nav > .toolbar-item > a.active { 53 | color: #000; 54 | } 55 | 56 | .toolbar-item a { 57 | cursor: pointer; 58 | } 59 | 60 | .view { 61 | 62 | } 63 | 64 | .indent { 65 | margin-left:5px; 66 | } 67 | 68 | .card-container { 69 | width:85%; 70 | } 71 | 72 | .card { 73 | background-color:#fff; 74 | border: 1px solid #d4d4d4; 75 | height:100px; 76 | margin-bottom: 20px; 77 | position: relative; 78 | } 79 | 80 | .card-header { 81 | background-color:#027FF4; 82 | font-size:14pt; 83 | color:white; 84 | padding:5px; 85 | width:100%; 86 | } 87 | 88 | .card-close { 89 | color: white; 90 | font-weight:bold; 91 | margin-right:5px; 92 | } 93 | 94 | .card-body { 95 | padding-left: 5px; 96 | } 97 | 98 | .card-body-left { 99 | margin-top: -5px; 100 | } 101 | 102 | .card-body-right { 103 | margin-left: 20px; 104 | margin-top: 2px; 105 | } 106 | 107 | .card-body-content { 108 | width: 100px; 109 | } 110 | 111 | .card-image { 112 | height:50px;width:50px;margin-top:10px; 113 | } 114 | 115 | .grid-container div { 116 | padding-left: 0px; 117 | } 118 | 119 | .grid-container td { 120 | vertical-align: middle; 121 | } 122 | 123 | .navbar-brand { 124 | float:none; 125 | } 126 | 127 | a.navbar-brand { 128 | color: #fff; 129 | } 130 | 131 | .navbar-inner { 132 | padding-left: 0px; 133 | -webkit-border-radius: 0px; 134 | border-radius: 0px; 135 | -webkit-box-shadow: none; 136 | -moz-box-shadow: none; 137 | box-shadow: none; 138 | background-color: #027FF4; 139 | background-image: none; 140 | } 141 | 142 | .navbar-inner.toolbar { 143 | background-color: #fafafa; 144 | } 145 | 146 | footer { 147 | margin-top: 10px; 148 | } 149 | 150 | .navbar-inner.footer { 151 | background-color: #fafafa; 152 | -webkit-box-shadow: none; 153 | -moz-box-shadow: none; 154 | box-shadow: none; 155 | height:50px; 156 | } 157 | 158 | .navbar .nav > .active > a, .navbar .nav > .active > a:hover, .navbar .nav > .active > a:focus { 159 | background-color: #efefef; 160 | -webkit-box-shadow: none; 161 | box-shadow: none; 162 | color: #808080; 163 | } 164 | 165 | .navbar .nav li.toolbaritem a:hover, .navbar .nav li a:hover { 166 | color: #E03930; 167 | } 168 | 169 | .navbar .nav > li { 170 | cursor:pointer; 171 | } 172 | 173 | .navbar .nav > li > a { 174 | color: white; 175 | font-weight:bold; 176 | -webkit-text-shadow: none; 177 | text-shadow: none; 178 | height:30px; 179 | padding-top: 6px; 180 | padding-bottom: 0px; 181 | } 182 | 183 | .navbar .nav > li.toolbaritem > a { 184 | color: black; 185 | font-weight:bold; 186 | -webkit-text-shadow: none; 187 | text-shadow: none; 188 | } 189 | 190 | 191 | .navbar-fixed-top .navbar-inner, 192 | .navbar-static-top .navbar-inner { 193 | -webkit-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 194 | -moz-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 195 | box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 196 | } 197 | 198 | .nav.navBarPadding { 199 | margin-left:25px; 200 | margin-top: 10px; 201 | } 202 | 203 | .navbarText { 204 | font-weight:bold; 205 | } 206 | 207 | .navbar .brand { 208 | margin-top: 2px; 209 | color: #fff; 210 | -webkit-text-shadow: none; 211 | text-shadow: none; 212 | } 213 | 214 | .navbar-toggle { 215 | border: 1px solid white; 216 | } 217 | 218 | .navbar-toggle .icon-bar { 219 | background-color: white; 220 | } 221 | 222 | footer { 223 | margin-top: 15px; 224 | } 225 | 226 | .navbar-inner.footer { 227 | background-color: #fafafa; 228 | -webkit-box-shadow: none; 229 | -moz-box-shadow: none; 230 | box-shadow: none; 231 | height:50px; 232 | } 233 | 234 | filter-textbox { 235 | margin-top: 5px; 236 | } 237 | 238 | form { 239 | width:50%; 240 | } 241 | 242 | .editForm .ng-invalid { 243 | border-left: 5px solid #a94442; 244 | } 245 | 246 | .editForm .ng-valid { 247 | border-left: 5px solid #42A948; 248 | } 249 | 250 | -------------------------------------------------------------------------------- /src/app/core/data.service.ts.httpmodule: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | //Using the newer HttpClientModule now. 4 | //This is the pre-Angular 4.3 Http option. If you're not on Angular 4.3 yet, 5 | //simplify rename this file to data.service.ts to use it instead. 6 | import { Http, Headers, Response, RequestOptions } from '@angular/http'; 7 | 8 | //Grab everything with import 'rxjs/Rx'; 9 | import { Observable } from 'rxjs/Observable'; 10 | import 'rxjs/add/observable/throw'; 11 | import 'rxjs/add/operator/map'; 12 | import 'rxjs/add/operator/catch'; 13 | 14 | import { ICustomer, IOrder, IState, IPagedResults } from '../shared/interfaces'; 15 | 16 | @Injectable() 17 | export class DataService { 18 | 19 | baseUrl: string = '/api/customers'; 20 | 21 | constructor(private http: Http) { 22 | 23 | } 24 | 25 | getCustomers() : Observable { 26 | return this.http.get(this.baseUrl) 27 | .map((res: Response) => { 28 | let customers = res.json(); 29 | this.calculateCustomersOrderTotal(customers); 30 | return customers; 31 | }) 32 | .catch(this.handleError); 33 | } 34 | 35 | getCustomersPage(page: number, pageSize: number) : Observable> { 36 | return this.http.get(`${this.baseUrl}/page/${page}/${pageSize}`) 37 | .map((res: Response) => { 38 | const totalRecords = +res.headers.get('x-inlinecount'); 39 | let customers = res.json(); 40 | this.calculateCustomersOrderTotal(customers); 41 | return { 42 | results: customers, 43 | totalRecords: totalRecords 44 | }; 45 | }) 46 | .catch(this.handleError); 47 | } 48 | 49 | getCustomer(id: string) : Observable { 50 | return this.http.get(this.baseUrl + '/' + id) 51 | .map((res: Response) => res.json()) 52 | .catch(this.handleError); 53 | } 54 | 55 | insertCustomer(customer: ICustomer) : Observable { 56 | return this.http.post(this.baseUrl, customer) 57 | .map((res: Response) => { 58 | const data = res.json(); 59 | console.log('insertCustomer status: ' + data.status); 60 | return data.customer; 61 | }) 62 | .catch(this.handleError); 63 | } 64 | 65 | updateCustomer(customer: ICustomer) : Observable { 66 | return this.http.put(this.baseUrl + '/' + customer._id, customer) 67 | .map((res: Response) => { 68 | const data = res.json(); 69 | console.log('updateCustomer status: ' + data.status); 70 | return data.customer; 71 | }) 72 | .catch(this.handleError); 73 | } 74 | 75 | deleteCustomer(id: string) : Observable { 76 | return this.http.delete(this.baseUrl + '/' + id) 77 | .map((res: Response) => res.json().status) 78 | .catch(this.handleError); 79 | } 80 | 81 | //Not used but could be called to pass "options" (3rd parameter) to 82 | //appropriate POST/PUT/DELETE calls made with http 83 | getRequestOptions() { 84 | const csrfToken = ''; //would retrieve from cookie or from page 85 | const options = new RequestOptions({ 86 | headers: new Headers({ 'x-xsrf-token': csrfToken }) 87 | }); 88 | return options; 89 | } 90 | 91 | getStates(): Observable { 92 | return this.http.get('/api/states') 93 | .map((res: Response) => res.json()) 94 | .catch(this.handleError); 95 | } 96 | 97 | calculateCustomersOrderTotal(customers: ICustomer[]) { 98 | for (let customer of customers) { 99 | if (customer && customer.orders) { 100 | let total = 0; 101 | for (let order of customer.orders) { 102 | total += (order.price * order.quantity); 103 | } 104 | customer.orderTotal = total; 105 | } 106 | } 107 | } 108 | 109 | private handleError(error: any) { 110 | console.error('server error:', error); 111 | if (error instanceof Response) { 112 | let errMessage = ''; 113 | try { 114 | errMessage = error.json().error; 115 | } catch(err) { 116 | errMessage = error.statusText; 117 | } 118 | return Observable.throw(errMessage); 119 | // Use the following instead if using lite-server 120 | //return Observable.throw(err.text() || 'backend server error'); 121 | } 122 | return Observable.throw(error || 'Node.js server error'); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /controllers/api/customers/customers.controller.js: -------------------------------------------------------------------------------- 1 | const customersRepo = require('../../../lib/customersRepository'), 2 | statesRepo = require('../../../lib/statesRepository'), 3 | util = require('util'); 4 | 5 | class CustomersController { 6 | 7 | constructor(router) { 8 | router.get('/', this.getCustomers.bind(this)); 9 | router.get('/page/:skip/:top', this.getCustomersPage.bind(this)); 10 | router.get('/:id', this.getCustomer.bind(this)); 11 | router.post('/', this.insertCustomer.bind(this)); 12 | router.put('/:id', this.updateCustomer.bind(this)); 13 | router.delete('/:id', this.deleteCustomer.bind(this)); 14 | } 15 | 16 | getCustomers(req, res) { 17 | console.log('*** getCustomers'); 18 | customersRepo.getCustomers((err, data) => { 19 | if (err) { 20 | console.log('*** getCustomers error: ' + util.inspect(err)); 21 | res.json(null); 22 | } else { 23 | console.log('*** getCustomers ok'); 24 | res.json(data.customers); 25 | } 26 | }); 27 | } 28 | 29 | getCustomersPage(req, res) { 30 | console.log('*** getCustomersPage'); 31 | const topVal = req.params.top, 32 | skipVal = req.params.skip, 33 | top = (isNaN(topVal)) ? 10 : +topVal, 34 | skip = (isNaN(skipVal)) ? 0 : +skipVal; 35 | 36 | customersRepo.getPagedCustomers(skip, top, (err, data) => { 37 | res.setHeader('X-InlineCount', data.count); 38 | if (err) { 39 | console.log('*** getCustomersPage error: ' + util.inspect(err)); 40 | res.json(null); 41 | } else { 42 | console.log('*** getCustomersPage ok'); 43 | res.json(data.customers); 44 | } 45 | }); 46 | } 47 | 48 | getCustomer(req, res) { 49 | console.log('*** getCustomer'); 50 | const id = req.params.id; 51 | console.log(id); 52 | 53 | customersRepo.getCustomer(id, (err, customer) => { 54 | if (err) { 55 | console.log('*** getCustomer error: ' + util.inspect(err)); 56 | res.json(null); 57 | } else { 58 | console.log('*** getCustomer ok'); 59 | res.json(customer); 60 | } 61 | }); 62 | } 63 | 64 | insertCustomer(req, res) { 65 | console.log('*** insertCustomer'); 66 | statesRepo.getState(req.body.stateId, (err, state) => { 67 | if (err) { 68 | console.log('*** statesRepo.getState error: ' + util.inspect(err)); 69 | res.json({ status: false, error: 'State not found', customer: null }); 70 | } else { 71 | customersRepo.insertCustomer(req.body, state, (err, customer) => { 72 | if (err) { 73 | console.log('*** customersRepo.insertCustomer error: ' + util.inspect(err)); 74 | res.json({status: false, error: 'Insert failed', customer: null}); 75 | } else { 76 | console.log('*** insertCustomer ok'); 77 | res.json({ status: true, error: null, customer: customer }); 78 | } 79 | }); 80 | } 81 | }); 82 | } 83 | 84 | updateCustomer(req, res) { 85 | console.log('*** updateCustomer'); 86 | console.log('*** req.body'); 87 | console.log(req.body); 88 | 89 | if (!req.body || !req.body.stateId) { 90 | throw new Error('Customer and associated stateId required'); 91 | } 92 | 93 | statesRepo.getState(req.body.stateId, (err, state) => { 94 | if (err) { 95 | console.log('*** statesRepo.getState error: ' + util.inspect(err)); 96 | res.json({ status: false, error: 'State not found', customer: null }); 97 | } else { 98 | customersRepo.updateCustomer(req.params.id, req.body, state, (err, customer) => { 99 | if (err) { 100 | console.log('*** updateCustomer error: ' + util.inspect(err)); 101 | res.json({ status: false, error: 'Update failed', customer: null }); 102 | } else { 103 | console.log('*** updateCustomer ok'); 104 | res.json({ status: true, error: null, customer: customer }); 105 | } 106 | }); 107 | } 108 | }); 109 | } 110 | 111 | deleteCustomer(req, res) { 112 | console.log('*** deleteCustomer'); 113 | 114 | customersRepo.deleteCustomer(req.params.id, (err) => { 115 | if (err) { 116 | console.log('*** deleteCustomer error: ' + util.inspect(err)); 117 | res.json({ status: false }); 118 | } else { 119 | console.log('*** deleteCustomer ok'); 120 | res.json({ status: true }); 121 | } 122 | }); 123 | } 124 | 125 | } 126 | 127 | module.exports = CustomersController; -------------------------------------------------------------------------------- /lib/customersRepository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | Customer = require('../models/customer'); 4 | 5 | class CustomersRepository { 6 | 7 | // get all the customers 8 | getCustomers(callback) { 9 | console.log('*** CustomersRepository.getCustomers'); 10 | Customer.count((err, custsCount) => { 11 | let count = custsCount; 12 | console.log(`Customers count: ${count}`); 13 | 14 | Customer.find({}, (err, customers) => { 15 | if (err) { 16 | console.log(`*** CustomersRepository.getCustomers error: ${err}`); 17 | return callback(err); 18 | } 19 | callback(null, { 20 | count: count, 21 | customers: customers 22 | }); 23 | }); 24 | 25 | }); 26 | } 27 | 28 | getPagedCustomers(skip, top, callback) { 29 | console.log('*** CustomersRepository.getPagedCustomers'); 30 | Customer.count((err, custsCount) => { 31 | let count = custsCount; 32 | console.log(`Skip: ${skip} Top: ${top}`); 33 | console.log(`Customers count: ${count}`); 34 | 35 | Customer.find({}) 36 | .sort({lastName: 1}) 37 | .skip(skip) 38 | .limit(top) 39 | .exec((err, customers) => { 40 | if (err) { 41 | console.log(`*** CustomersRepository.getPagedCustomers error: ${err}`); 42 | return callback(err); 43 | } 44 | callback(null, { 45 | count: count, 46 | customers: customers 47 | }); 48 | }); 49 | 50 | }); 51 | } 52 | 53 | // get the customer summary 54 | getCustomersSummary(skip, top, callback) { 55 | console.log('*** CustomersRepository.getCustomersSummary'); 56 | Customer.count((err, custsCount) => { 57 | let count = custsCount; 58 | console.log(`Customers count: ${count}`); 59 | 60 | Customer.find({}, { '_id': 0, 'firstName': 1, 'lastName': 1, 'city': 1, 'state': 1, 'orderCount': 1, 'gender': 1 }) 61 | .skip(skip) 62 | .limit(top) 63 | .exec((err, customersSummary) => { 64 | callback(null, { 65 | count: count, 66 | customersSummary: customersSummary 67 | }); 68 | }); 69 | 70 | }); 71 | } 72 | 73 | // get a customer 74 | getCustomer(id, callback) { 75 | console.log('*** CustomersRepository.getCustomer'); 76 | Customer.findById(id, (err, customer) => { 77 | if (err) { 78 | console.log(`*** CustomersRepository.getCustomer error: ${err}`); 79 | return callback(err); 80 | } 81 | callback(null, customer); 82 | }); 83 | } 84 | 85 | // insert a customer 86 | insertCustomer(body, state, callback) { 87 | console.log('*** CustomersRepository.insertCustomer'); 88 | console.log(state); 89 | let customer = new Customer(); 90 | let newState = { 'id': state[0].id, 'abbreviation': state[0].abbreviation, 'name': state[0].name } 91 | console.log(body); 92 | 93 | customer.firstName = body.firstName; 94 | customer.lastName = body.lastName; 95 | customer.email = body.email; 96 | customer.address = body.address; 97 | customer.city = body.city; 98 | customer.state = newState; 99 | customer.stateId = newState.id; 100 | customer.zip = body.zip; 101 | customer.gender = body.gender; 102 | 103 | customer.save((err, customer) => { 104 | if (err) { 105 | console.log(`*** CustomersRepository insertCustomer error: ${err}`); 106 | return callback(err, null); 107 | } 108 | 109 | callback(null, customer); 110 | }); 111 | } 112 | 113 | updateCustomer(id, body, state, callback) { 114 | console.log('*** CustomersRepository.editCustomer'); 115 | 116 | let stateObj = { 'id': state[0].id, 'abbreviation': state[0].abbreviation, 'name': state[0].name } 117 | 118 | Customer.findById(id, (err, customer) => { 119 | if (err) { 120 | console.log(`*** CustomersRepository.editCustomer error: ${err}`); 121 | return callback(err); 122 | } 123 | 124 | customer.firstName = body.firstName || customer.firstName; 125 | customer.lastName = body.lastName || customer.lastName; 126 | customer.email = body.email || customer.email; 127 | customer.address = body.address || customer.address; 128 | customer.city = body.city || customer.city; 129 | customer.state = stateObj; 130 | customer.stateId = stateObj.id; 131 | customer.zip = body.zip || customer.zip; 132 | customer.gender = body.gender || customer.gender; 133 | 134 | 135 | customer.save((err, customer) => { 136 | if (err) { 137 | console.log(`*** CustomersRepository.updateCustomer error: ${err}`); 138 | return callback(err, null); 139 | } 140 | 141 | callback(null, customer); 142 | }); 143 | 144 | }); 145 | } 146 | 147 | // delete a customer 148 | deleteCustomer(id, callback) { 149 | console.log('*** CustomersRepository.deleteCustomer'); 150 | Customer.remove({ '_id': id }, (err, customer) => { 151 | if (err) { 152 | console.log(`*** CustomersRepository.deleteCustomer error: ${err}`); 153 | return callback(err, null); 154 | } 155 | callback(null, customer); 156 | }); 157 | } 158 | 159 | } 160 | 161 | module.exports = new CustomersRepository(); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular TypeScript App 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 |
29 | 30 | Loading... 31 | 32 |

33 |
34 | 35 |
36 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/dbSeeder.js: -------------------------------------------------------------------------------- 1 | // Module dependencies 2 | const mongoose = require('mongoose'), 3 | Customer = require('../models/customer'), 4 | State = require('../models/state'), 5 | dbConfig = require('./configLoader').databaseConfig, 6 | connectionString = `mongodb://${dbConfig.host}/${dbConfig.database}`, 7 | connection = null; 8 | 9 | class DBSeeder { 10 | 11 | init() { 12 | mongoose.connection.db.listCollections({name: 'customers'}) 13 | .next((err, collinfo) => { 14 | if (!collinfo) { 15 | console.log('Starting dbSeeder...'); 16 | this.seed(); 17 | } 18 | }); 19 | } 20 | 21 | seed() { 22 | 23 | console.log('Seeding data....'); 24 | 25 | //Customers 26 | var customerNames = 27 | [ 28 | "Marcus,HighTower,Male,acmecorp.com", 29 | "Jesse,Smith,Female,gmail.com", 30 | "Albert,Einstein,Male,outlook.com", 31 | "Dan,Wahlin,Male,yahoo.com", 32 | "Ward,Bell,Male,gmail.com", 33 | "Brad,Green,Male,gmail.com", 34 | "Igor,Minar,Male,gmail.com", 35 | "Miško,Hevery,Male,gmail.com", 36 | "Michelle,Avery,Female,acmecorp.com", 37 | "Heedy,Wahlin,Female,hotmail.com", 38 | "Thomas,Martin,Male,outlook.com", 39 | "Jean,Martin,Female,outlook.com", 40 | "Robin,Cleark,Female,acmecorp.com", 41 | "Juan,Paulo,Male,yahoo.com", 42 | "Gene,Thomas,Male,gmail.com", 43 | "Pinal,Dave,Male,gmail.com", 44 | "Fred,Roberts,Male,outlook.com", 45 | "Tina,Roberts,Female,outlook.com", 46 | "Cindy,Jamison,Female,gmail.com", 47 | "Robyn,Flores,Female,yahoo.com", 48 | "Jeff,Wahlin,Male,gmail.com", 49 | "Danny,Wahlin,Male,gmail.com", 50 | "Elaine,Jones,Female,yahoo.com", 51 | "John,Papa,Male,gmail.com" 52 | ]; 53 | var addresses = 54 | [ 55 | "1234 Anywhere St.", 56 | "435 Main St.", 57 | "1 Atomic St.", 58 | "85 Cedar Dr.", 59 | "12 Ocean View St.", 60 | "1600 Amphitheatre Parkway", 61 | "1604 Amphitheatre Parkway", 62 | "1607 Amphitheatre Parkway", 63 | "346 Cedar Ave.", 64 | "4576 Main St.", 65 | "964 Point St.", 66 | "98756 Center St.", 67 | "35632 Richmond Circle Apt B", 68 | "2352 Angular Way", 69 | "23566 Directive Pl.", 70 | "235235 Yaz Blvd.", 71 | "7656 Crescent St.", 72 | "76543 Moon Ave.", 73 | "84533 Hardrock St.", 74 | "5687534 Jefferson Way", 75 | "346346 Blue Pl.", 76 | "23423 Adams St.", 77 | "633 Main St.", 78 | "899 Mickey Way" 79 | ]; 80 | 81 | var citiesStates = 82 | [ 83 | "Phoenix,AZ,Arizona", 84 | "Encinitas,CA,California", 85 | "Seattle,WA,Washington", 86 | "Chandler,AZ,Arizona", 87 | "Dallas,TX,Texas", 88 | "Orlando,FL,Florida", 89 | "Carey,NC,North Carolina", 90 | "Anaheim,CA,California", 91 | "Dallas,TX,Texas", 92 | "New York,NY,New York", 93 | "White Plains,NY,New York", 94 | "Las Vegas,NV,Nevada", 95 | "Los Angeles,CA,California", 96 | "Portland,OR,Oregon", 97 | "Seattle,WA,Washington", 98 | "Houston,TX,Texas", 99 | "Chicago,IL,Illinois", 100 | "Atlanta,GA,Georgia", 101 | "Chandler,AZ,Arizona", 102 | "Buffalo,NY,New York", 103 | "Albuquerque,AZ,Arizona", 104 | "Boise,ID,Idaho", 105 | "Salt Lake City,UT,Utah", 106 | "Orlando,FL,Florida" 107 | ]; 108 | 109 | var citiesIds = [5, 9, 44, 5, 36, 17, 16, 9, 36, 14, 14, 6, 9, 24, 44, 36, 25, 19, 5, 14, 5, 23, 38, 17]; 110 | 111 | 112 | var zip = 85229; 113 | 114 | var orders = 115 | [ 116 | { "product": "Basket", "price": 29.99, "quantity": 1 }, 117 | { "product": "Yarn", "price": 9.99, "quantity": 1 }, 118 | { "product": "Needes", "price": 5.99, "quantity": 1 }, 119 | { "product": "Speakers", "price": 499.99, "quantity": 1 }, 120 | { "product": "iPod", "price": 399.99, "quantity": 1 }, 121 | { "product": "Table", "price": 329.99, "quantity": 1 }, 122 | { "product": "Chair", "price": 129.99, "quantity": 4 }, 123 | { "product": "Lamp", "price": 89.99, "quantity": 5 }, 124 | { "product": "Call of Duty", "price": 59.99, "quantity": 1 }, 125 | { "product": "Controller", "price": 49.99, "quantity": 1 }, 126 | { "product": "Gears of War", "price": 49.99, "quantity": 1 }, 127 | { "product": "Lego City", "price": 49.99, "quantity": 1 }, 128 | { "product": "Baseball", "price": 9.99, "quantity": 5 }, 129 | { "product": "Bat", "price": 19.99, "quantity": 1 } 130 | ]; 131 | 132 | Customer.remove({}); 133 | 134 | var l = customerNames.length, 135 | i, 136 | j, 137 | firstOrder, 138 | lastOrder, 139 | tempOrder, 140 | n = orders.length; 141 | 142 | for (i = 0; i < l; i++) { 143 | var nameGenderHost = customerNames[i].split(','); 144 | var cityState = citiesStates[i].split(','); 145 | var state = { 'id': citiesIds[i], 'abbreviation': cityState[1], 'name': cityState[2] }; 146 | var customer = new Customer({ 147 | 'firstName': nameGenderHost[0], 148 | 'lastName': nameGenderHost[1], 149 | 'email': nameGenderHost[0] + '.' + nameGenderHost[1] + '@' + nameGenderHost[3], 150 | 'address': addresses[i], 151 | 'city': cityState[0], 152 | 'state': state, 153 | 'stateId': citiesIds[i], 154 | 'zip': zip + i, 155 | 'gender': nameGenderHost[2], 156 | 'orderCount': 0 157 | }); 158 | firstOrder = Math.floor(Math.random() * orders.length); 159 | lastOrder = Math.floor(Math.random() * orders.length); 160 | if (firstOrder > lastOrder) { 161 | tempOrder = firstOrder; 162 | firstOrder = lastOrder; 163 | lastOrder = tempOrder; 164 | } 165 | 166 | customer.orders = []; 167 | //console.log('firstOrder: ' + firstOrder + ", lastOrder: " + lastOrder); 168 | for (j = firstOrder; j <= lastOrder && j < n; j++) { 169 | var today = new Date(); 170 | var tomorrow = new Date(); 171 | tomorrow.setDate(today.getDate() + (Math.random() * 100)); 172 | 173 | var o = { 174 | "product": orders[j].product, 175 | "price": orders[j].price, 176 | "quantity": orders[j].quantity, 177 | "date": tomorrow 178 | }; 179 | customer.orders.push(o); 180 | } 181 | customer.orderCount = customer.orders.length; 182 | 183 | customer.save((err, cust) => { 184 | if (err) { 185 | console.log(err); 186 | } else { 187 | console.log('inserted customer: ' + cust.firstName + ' ' + cust.lastName); 188 | } 189 | }); 190 | } 191 | 192 | //States 193 | var states = [ 194 | { "name": "Alabama", "abbreviation": "AL" }, 195 | { "name": "Montana", "abbreviation": "MT" }, 196 | { "name": "Alaska", "abbreviation": "AK" }, 197 | { "name": "Nebraska", "abbreviation": "NE" }, 198 | { "name": "Arizona", "abbreviation": "AZ" }, 199 | { "name": "Nevada", "abbreviation": "NV" }, 200 | { "name": "Arkansas", "abbreviation": "AR" }, 201 | { "name": "New Hampshire", "abbreviation": "NH" }, 202 | { "name": "California", "abbreviation": "CA" }, 203 | { "name": "New Jersey", "abbreviation": "NJ" }, 204 | { "name": "Colorado", "abbreviation": "CO" }, 205 | { "name": "New Mexico", "abbreviation": "NM" }, 206 | { "name": "Connecticut", "abbreviation": "CT" }, 207 | { "name": "New York", "abbreviation": "NY" }, 208 | { "name": "Delaware", "abbreviation": "DE" }, 209 | { "name": "North Carolina", "abbreviation": "NC" }, 210 | { "name": "Florida", "abbreviation": "FL" }, 211 | { "name": "North Dakota", "abbreviation": "ND" }, 212 | { "name": "Georgia", "abbreviation": "GA" }, 213 | { "name": "Ohio", "abbreviation": "OH" }, 214 | { "name": "Hawaii", "abbreviation": "HI" }, 215 | { "name": "Oklahoma", "abbreviation": "OK" }, 216 | { "name": "Idaho", "abbreviation": "ID" }, 217 | { "name": "Oregon", "abbreviation": "OR" }, 218 | { "name": "Illinois", "abbreviation": "IL" }, 219 | { "name": "Pennsylvania", "abbreviation": "PA" }, 220 | { "name": "Indiana", "abbreviation": "IN" }, 221 | { "name": "Rhode Island", "abbreviation": "RI" }, 222 | { "name": "Iowa", "abbreviation": "IA" }, 223 | { "name": "South Carolina", "abbreviation": "SC" }, 224 | { "name": "Kansas", "abbreviation": "KS" }, 225 | { "name": "South Dakota", "abbreviation": "SD" }, 226 | { "name": "Kentucky", "abbreviation": "KY" }, 227 | { "name": "Tennessee", "abbreviation": "TN" }, 228 | { "name": "Louisiana", "abbreviation": "LA" }, 229 | { "name": "Texas", "abbreviation": "TX" }, 230 | { "name": "Maine", "abbreviation": "ME" }, 231 | { "name": "Utah", "abbreviation": "UT" }, 232 | { "name": "Maryland", "abbreviation": "MD" }, 233 | { "name": "Vermont", "abbreviation": "VT" }, 234 | { "name": "Massachusetts", "abbreviation": "MA" }, 235 | { "name": "Virginia", "abbreviation": "VA" }, 236 | { "name": "Michigan", "abbreviation": "MI" }, 237 | { "name": "Washington", "abbreviation": "WA" }, 238 | { "name": "Minnesota", "abbreviation": "MN" }, 239 | { "name": "West Virginia", "abbreviation": "WV" }, 240 | { "name": "Mississippi", "abbreviation": "MS" }, 241 | { "name": "Wisconsin", "abbreviation": "WI" }, 242 | { "name": "Missouri", "abbreviation": "MO" }, 243 | { "name": "Wyoming", "abbreviation": "WY" } 244 | ]; 245 | 246 | var l = states.length, 247 | i; 248 | 249 | State.remove({}); 250 | 251 | for (i = 0; i < l; i++) { 252 | var state = new State ({ 'id': i + 1, 'name': states[i].name, 'abbreviation': states[i].abbreviation }); 253 | state.save(); 254 | } 255 | } 256 | } 257 | 258 | module.exports = new DBSeeder(); 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /public/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @angular/common 2 | MIT 3 | 4 | @angular/core 5 | MIT 6 | 7 | @angular/forms 8 | MIT 9 | 10 | @angular/platform-browser 11 | MIT 12 | 13 | @angular/router 14 | MIT 15 | 16 | rxjs 17 | Apache-2.0 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, 27 | and distribution as defined by Sections 1 through 9 of this document. 28 | 29 | "Licensor" shall mean the copyright owner or entity authorized by 30 | the copyright owner that is granting the License. 31 | 32 | "Legal Entity" shall mean the union of the acting entity and all 33 | other entities that control, are controlled by, or are under common 34 | control with that entity. For the purposes of this definition, 35 | "control" means (i) the power, direct or indirect, to cause the 36 | direction or management of such entity, whether by contract or 37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | "You" (or "Your") shall mean an individual or Legal Entity 41 | exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical 48 | transformation or translation of a Source form, including but 49 | not limited to compiled object code, generated documentation, 50 | and conversions to other media types. 51 | 52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 | 57 | "Derivative Works" shall mean any work, whether in Source or Object 58 | form, that is based on (or derived from) the Work and for which the 59 | editorial revisions, annotations, elaborations, or other modifications 60 | represent, as a whole, an original work of authorship. For the purposes 61 | of this License, Derivative Works shall not include works that remain 62 | separable from, or merely link (or bind by name) to the interfaces of, 63 | the Work and Derivative Works thereof. 64 | 65 | "Contribution" shall mean any work of authorship, including 66 | the original version of the Work and any modifications or additions 67 | to that Work or Derivative Works thereof, that is intentionally 68 | submitted to Licensor for inclusion in the Work by the copyright owner 69 | or by an individual or Legal Entity authorized to submit on behalf of 70 | the copyright owner. For the purposes of this definition, "submitted" 71 | means any form of electronic, verbal, or written communication sent 72 | to the Licensor or its representatives, including but not limited to 73 | communication on electronic mailing lists, source code control systems, 74 | and issue tracking systems that are managed by, or on behalf of, the 75 | Licensor for the purpose of discussing and improving the Work, but 76 | excluding communication that is conspicuously marked or otherwise 77 | designated in writing by the copyright owner as "Not a Contribution." 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity 80 | on behalf of whom a Contribution has been received by Licensor and 81 | subsequently incorporated within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | copyright license to reproduce, prepare Derivative Works of, 87 | publicly display, publicly perform, sublicense, and distribute the 88 | Work and such Derivative Works in Source or Object form. 89 | 90 | 3. Grant of Patent License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | (except as stated in this section) patent license to make, have made, 94 | use, offer to sell, sell, import, and otherwise transfer the Work, 95 | where such license applies only to those patent claims licensable 96 | by such Contributor that are necessarily infringed by their 97 | Contribution(s) alone or by combination of their Contribution(s) 98 | with the Work to which such Contribution(s) was submitted. If You 99 | institute patent litigation against any entity (including a 100 | cross-claim or counterclaim in a lawsuit) alleging that the Work 101 | or a Contribution incorporated within the Work constitutes direct 102 | or contributory patent infringement, then any patent licenses 103 | granted to You under this License for that Work shall terminate 104 | as of the date such litigation is filed. 105 | 106 | 4. Redistribution. You may reproduce and distribute copies of the 107 | Work or Derivative Works thereof in any medium, with or without 108 | modifications, and in Source or Object form, provided that You 109 | meet the following conditions: 110 | 111 | (a) You must give any other recipients of the Work or 112 | Derivative Works a copy of this License; and 113 | 114 | (b) You must cause any modified files to carry prominent notices 115 | stating that You changed the files; and 116 | 117 | (c) You must retain, in the Source form of any Derivative Works 118 | that You distribute, all copyright, patent, trademark, and 119 | attribution notices from the Source form of the Work, 120 | excluding those notices that do not pertain to any part of 121 | the Derivative Works; and 122 | 123 | (d) If the Work includes a "NOTICE" text file as part of its 124 | distribution, then any Derivative Works that You distribute must 125 | include a readable copy of the attribution notices contained 126 | within such NOTICE file, excluding those notices that do not 127 | pertain to any part of the Derivative Works, in at least one 128 | of the following places: within a NOTICE text file distributed 129 | as part of the Derivative Works; within the Source form or 130 | documentation, if provided along with the Derivative Works; or, 131 | within a display generated by the Derivative Works, if and 132 | wherever such third-party notices normally appear. The contents 133 | of the NOTICE file are for informational purposes only and 134 | do not modify the License. You may add Your own attribution 135 | notices within Derivative Works that You distribute, alongside 136 | or as an addendum to the NOTICE text from the Work, provided 137 | that such additional attribution notices cannot be construed 138 | as modifying the License. 139 | 140 | You may add Your own copyright statement to Your modifications and 141 | may provide additional or different license terms and conditions 142 | for use, reproduction, or distribution of Your modifications, or 143 | for any such Derivative Works as a whole, provided Your use, 144 | reproduction, and distribution of the Work otherwise complies with 145 | the conditions stated in this License. 146 | 147 | 5. Submission of Contributions. Unless You explicitly state otherwise, 148 | any Contribution intentionally submitted for inclusion in the Work 149 | by You to the Licensor shall be under the terms and conditions of 150 | this License, without any additional terms or conditions. 151 | Notwithstanding the above, nothing herein shall supersede or modify 152 | the terms of any separate license agreement you may have executed 153 | with Licensor regarding such Contributions. 154 | 155 | 6. Trademarks. This License does not grant permission to use the trade 156 | names, trademarks, service marks, or product names of the Licensor, 157 | except as required for reasonable and customary use in describing the 158 | origin of the Work and reproducing the content of the NOTICE file. 159 | 160 | 7. Disclaimer of Warranty. Unless required by applicable law or 161 | agreed to in writing, Licensor provides the Work (and each 162 | Contributor provides its Contributions) on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 164 | implied, including, without limitation, any warranties or conditions 165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 166 | PARTICULAR PURPOSE. You are solely responsible for determining the 167 | appropriateness of using or redistributing the Work and assume any 168 | risks associated with Your exercise of permissions under this License. 169 | 170 | 8. Limitation of Liability. In no event and under no legal theory, 171 | whether in tort (including negligence), contract, or otherwise, 172 | unless required by applicable law (such as deliberate and grossly 173 | negligent acts) or agreed to in writing, shall any Contributor be 174 | liable to You for damages, including any direct, indirect, special, 175 | incidental, or consequential damages of any character arising as a 176 | result of this License or out of the use or inability to use the 177 | Work (including but not limited to damages for loss of goodwill, 178 | work stoppage, computer failure or malfunction, or any and all 179 | other commercial damages or losses), even if such Contributor 180 | has been advised of the possibility of such damages. 181 | 182 | 9. Accepting Warranty or Additional Liability. While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | END OF TERMS AND CONDITIONS 194 | 195 | APPENDIX: How to apply the Apache License to your work. 196 | 197 | To apply the Apache License to your work, attach the following 198 | boilerplate notice, with the fields enclosed by brackets "[]" 199 | replaced with your own identifying information. (Don't include 200 | the brackets!) The text should be enclosed in the appropriate 201 | comment syntax for the file format. We also recommend that a 202 | file or class name and description of purpose be included on the 203 | same "printed page" as the copyright notice for easier 204 | identification within third-party archives. 205 | 206 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 207 | 208 | Licensed under the Apache License, Version 2.0 (the "License"); 209 | you may not use this file except in compliance with the License. 210 | You may obtain a copy of the License at 211 | 212 | http://www.apache.org/licenses/LICENSE-2.0 213 | 214 | Unless required by applicable law or agreed to in writing, software 215 | distributed under the License is distributed on an "AS IS" BASIS, 216 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 217 | See the License for the specific language governing permissions and 218 | limitations under the License. 219 | 220 | 221 | 222 | tslib 223 | 0BSD 224 | Copyright (c) Microsoft Corporation. 225 | 226 | Permission to use, copy, modify, and/or distribute this software for any 227 | purpose with or without fee is hereby granted. 228 | 229 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 230 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 231 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 232 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 233 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 234 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 235 | PERFORMANCE OF THIS SOFTWARE. 236 | 237 | zone.js 238 | MIT 239 | The MIT License 240 | 241 | Copyright (c) 2010-2020 Google LLC. https://angular.io/license 242 | 243 | Permission is hereby granted, free of charge, to any person obtaining a copy 244 | of this software and associated documentation files (the "Software"), to deal 245 | in the Software without restriction, including without limitation the rights 246 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 247 | copies of the Software, and to permit persons to whom the Software is 248 | furnished to do so, subject to the following conditions: 249 | 250 | The above copyright notice and this permission notice shall be included in 251 | all copies or substantial portions of the Software. 252 | 253 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 254 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 255 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 256 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 257 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 258 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 259 | THE SOFTWARE. 260 | -------------------------------------------------------------------------------- /public/polyfills.0ed53563d957f923.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkmy_project=self.webpackChunkmy_project||[]).push([[429],{435:(Ee,Pe,Oe)=>{Oe(609)},609:(Ee,Pe,Oe)=>{var De;void 0!==(De=function(){!function(e){var r=e.performance;function t(h){r&&r.mark&&r.mark(h)}function n(h,a){r&&r.measure&&r.measure(h,a)}t("Zone");var u=e.__Zone_symbol_prefix||"__zone_symbol__";function c(h){return u+h}var l=!0===e[c("forceDuplicateZoneCheck")];if(e.Zone){if(l||"function"!=typeof e.Zone.__symbol__)throw new Error("Zone already loaded.");return e.Zone}var v=function(){function h(a,o){this._parent=a,this._name=o?o.name||"unnamed":"",this._properties=o&&o.properties||{},this._zoneDelegate=new d(this,this._parent&&this._parent._zoneDelegate,o)}return h.assertZonePatched=function(){if(e.Promise!==F.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")},Object.defineProperty(h,"root",{get:function(){for(var a=h.current;a.parent;)a=a.parent;return a},enumerable:!1,configurable:!0}),Object.defineProperty(h,"current",{get:function(){return A.zone},enumerable:!1,configurable:!0}),Object.defineProperty(h,"currentTask",{get:function(){return ie},enumerable:!1,configurable:!0}),h.__load_patch=function(a,o,i){if(void 0===i&&(i=!1),F.hasOwnProperty(a)){if(!i&&l)throw Error("Already loaded patch: "+a)}else if(!e["__Zone_disable_"+a]){var P="Zone:"+a;t(P),F[a]=o(e,h,O),n(P,P)}},Object.defineProperty(h.prototype,"parent",{get:function(){return this._parent},enumerable:!1,configurable:!0}),Object.defineProperty(h.prototype,"name",{get:function(){return this._name},enumerable:!1,configurable:!0}),h.prototype.get=function(a){var o=this.getZoneWith(a);if(o)return o._properties[a]},h.prototype.getZoneWith=function(a){for(var o=this;o;){if(o._properties.hasOwnProperty(a))return o;o=o._parent}return null},h.prototype.fork=function(a){if(!a)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,a)},h.prototype.wrap=function(a,o){if("function"!=typeof a)throw new Error("Expecting function got: "+a);var i=this._zoneDelegate.intercept(this,a,o),P=this;return function(){return P.runGuarded(i,this,arguments,o)}},h.prototype.run=function(a,o,i,P){A={parent:A,zone:this};try{return this._zoneDelegate.invoke(this,a,o,i,P)}finally{A=A.parent}},h.prototype.runGuarded=function(a,o,i,P){void 0===o&&(o=null),A={parent:A,zone:this};try{try{return this._zoneDelegate.invoke(this,a,o,i,P)}catch(z){if(this._zoneDelegate.handleError(this,z))throw z}}finally{A=A.parent}},h.prototype.runTask=function(a,o,i){if(a.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(a.zone||B).name+"; Execution: "+this.name+")");if(a.state!==j||a.type!==R&&a.type!==Y){var P=a.state!=k;P&&a._transitionTo(k,U),a.runCount++;var z=ie;ie=a,A={parent:A,zone:this};try{a.type==Y&&a.data&&!a.data.isPeriodic&&(a.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,a,o,i)}catch(se){if(this._zoneDelegate.handleError(this,se))throw se}}finally{a.state!==j&&a.state!==I&&(a.type==R||a.data&&a.data.isPeriodic?P&&a._transitionTo(U,k):(a.runCount=0,this._updateTaskCount(a,-1),P&&a._transitionTo(j,k,j))),A=A.parent,ie=z}}},h.prototype.scheduleTask=function(a){if(a.zone&&a.zone!==this)for(var o=this;o;){if(o===a.zone)throw Error("can not reschedule task to "+this.name+" which is descendants of the original zone "+a.zone.name);o=o.parent}a._transitionTo(V,j);var i=[];a._zoneDelegates=i,a._zone=this;try{a=this._zoneDelegate.scheduleTask(this,a)}catch(P){throw a._transitionTo(I,V,j),this._zoneDelegate.handleError(this,P),P}return a._zoneDelegates===i&&this._updateTaskCount(a,1),a.state==V&&a._transitionTo(U,V),a},h.prototype.scheduleMicroTask=function(a,o,i,P){return this.scheduleTask(new p(ee,a,o,i,P,void 0))},h.prototype.scheduleMacroTask=function(a,o,i,P,z){return this.scheduleTask(new p(Y,a,o,i,P,z))},h.prototype.scheduleEventTask=function(a,o,i,P,z){return this.scheduleTask(new p(R,a,o,i,P,z))},h.prototype.cancelTask=function(a){if(a.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(a.zone||B).name+"; Execution: "+this.name+")");a._transitionTo($,U,k);try{this._zoneDelegate.cancelTask(this,a)}catch(o){throw a._transitionTo(I,$),this._zoneDelegate.handleError(this,o),o}return this._updateTaskCount(a,-1),a._transitionTo(j,$),a.runCount=0,a},h.prototype._updateTaskCount=function(a,o){var i=a._zoneDelegates;-1==o&&(a._zoneDelegates=null);for(var P=0;P0,macroTask:i.macroTask>0,eventTask:i.eventTask>0,change:a})},h}(),p=function(){function h(a,o,i,P,z,se){if(this._zone=null,this.runCount=0,this._zoneDelegates=null,this._state="notScheduled",this.type=a,this.source=o,this.data=P,this.scheduleFn=z,this.cancelFn=se,!i)throw new Error("callback is not defined");this.callback=i;var f=this;this.invoke=a===R&&P&&P.useG?h.invokeTask:function(){return h.invokeTask.call(e,f,this,arguments)}}return h.invokeTask=function(a,o,i){a||(a=this),ne++;try{return a.runCount++,a.zone.runTask(a,o,i)}finally{1==ne&&m(),ne--}},Object.defineProperty(h.prototype,"zone",{get:function(){return this._zone},enumerable:!1,configurable:!0}),Object.defineProperty(h.prototype,"state",{get:function(){return this._state},enumerable:!1,configurable:!0}),h.prototype.cancelScheduleRequest=function(){this._transitionTo(j,V)},h.prototype._transitionTo=function(a,o,i){if(this._state!==o&&this._state!==i)throw new Error(this.type+" '"+this.source+"': can not transition to '"+a+"', expecting state '"+o+"'"+(i?" or '"+i+"'":"")+", was '"+this._state+"'.");this._state=a,a==j&&(this._zoneDelegates=null)},h.prototype.toString=function(){return this.data&&void 0!==this.data.handleId?this.data.handleId.toString():Object.prototype.toString.call(this)},h.prototype.toJSON=function(){return{type:this.type,state:this.state,source:this.source,zone:this.zone.name,runCount:this.runCount}},h}(),y=c("setTimeout"),b=c("Promise"),w=c("then"),N=[],M=!1;function g(h){if(0===ne&&0===N.length)if(X||e[b]&&(X=e[b].resolve(0)),X){var a=X[w];a||(a=X.then),a.call(X,m)}else e[y](m,0);h&&N.push(h)}function m(){if(!M){for(M=!0;N.length;){var h=N;N=[];for(var a=0;a=0;t--)"function"==typeof e[t]&&(e[t]=ze(e[t],r+"_"+t));return e}function rr(e){return!e||!1!==e.writable&&!("function"==typeof e.get&&void 0===e.set)}var tr="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope,Ne=!("nw"in J)&&void 0!==J.process&&"[object process]"==={}.toString.call(J.process),qe=!Ne&&!tr&&!(!Ze||!ye.HTMLElement),nr=void 0!==J.process&&"[object process]"==={}.toString.call(J.process)&&!tr&&!(!Ze||!ye.HTMLElement),Le={},or=function(e){if(e=e||J.event){var r=Le[e.type];r||(r=Le[e.type]=G("ON_PROPERTY"+e.type));var u,t=this||e.target||J,n=t[r];return qe&&t===ye&&"error"===e.type?!0===(u=n&&n.call(this,e.message,e.filename,e.lineno,e.colno,e.error))&&e.preventDefault():null!=(u=n&&n.apply(this,arguments))&&!u&&e.preventDefault(),u}};function ar(e,r,t){var n=we(e,r);if(!n&&t&&we(t,r)&&(n={enumerable:!0,configurable:!0}),n&&n.configurable){var c=G("on"+r+"patched");if(!e.hasOwnProperty(c)||!e[c]){delete n.writable,delete n.value;var l=n.get,v=n.set,T=r.substr(2),d=Le[T];d||(d=Le[T]=G("ON_PROPERTY"+T)),n.set=function(p){var y=this;!y&&e===J&&(y=J),y&&(y[d]&&y.removeEventListener(T,or),v&&v.apply(y,Sr),"function"==typeof p?(y[d]=p,y.addEventListener(T,or,!1)):y[d]=null)},n.get=function(){var p=this;if(!p&&e===J&&(p=J),!p)return null;var y=p[d];if(y)return y;if(l){var b=l&&l.call(this);if(b)return n.set.call(this,b),"function"==typeof p.removeAttribute&&p.removeAttribute(r),b}return null},xe(e,r,n),e[c]=!0}}}function ir(e,r,t){if(r)for(var n=0;n=0&&"function"==typeof v[T.cbIdx]?We(T.name,v[T.cbIdx],T,u):c.apply(l,v)}})}function he(e,r){e[G("OriginalDelegate")]=r}var sr=!1,Ye=!1;function Zr(){if(sr)return Ye;sr=!0;try{var e=ye.navigator.userAgent;(-1!==e.indexOf("MSIE ")||-1!==e.indexOf("Trident/")||-1!==e.indexOf("Edge/"))&&(Ye=!0)}catch(r){}return Ye}Zone.__load_patch("ZoneAwarePromise",function(e,r,t){var n=Object.getOwnPropertyDescriptor,u=Object.defineProperty;var l=t.symbol,v=[],T=!0===e[l("DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION")],d=l("Promise"),p=l("then");t.onUnhandledError=function(f){if(t.showUncaughtError()){var E=f&&f.rejection;E?console.error("Unhandled Promise rejection:",E instanceof Error?E.message:E,"; Zone:",f.zone.name,"; Task:",f.task&&f.task.source,"; Value:",E,E instanceof Error?E.stack:void 0):console.error(f)}},t.microtaskDrainDone=function(){for(var f=function(){var E=v.shift();try{E.zone.runGuarded(function(){throw E.throwOriginal?E.rejection:E})}catch(s){!function w(f){t.onUnhandledError(f);try{var E=r[b];"function"==typeof E&&E.call(this,f)}catch(s){}}(s)}};v.length;)f()};var b=l("unhandledPromiseRejectionHandler");function N(f){return f&&f.then}function M(f){return f}function X(f){return o.reject(f)}var g=l("state"),m=l("value"),B=l("finally"),j=l("parentPromiseValue"),V=l("parentPromiseState"),k=null,$=!0,I=!1;function Y(f,E){return function(s){try{A(f,E,s)}catch(_){A(f,!1,_)}}}var O=l("currentTaskTrace");function A(f,E,s){var _=function(){var f=!1;return function(s){return function(){f||(f=!0,s.apply(null,arguments))}}}();if(f===s)throw new TypeError("Promise resolved with itself");if(f[g]===k){var S=null;try{("object"==typeof s||"function"==typeof s)&&(S=s&&s.then)}catch(Z){return _(function(){A(f,!1,Z)})(),f}if(E!==I&&s instanceof o&&s.hasOwnProperty(g)&&s.hasOwnProperty(m)&&s[g]!==k)ne(s),A(f,s[g],s[m]);else if(E!==I&&"function"==typeof S)try{S.call(s,_(Y(f,E)),_(Y(f,!1)))}catch(Z){_(function(){A(f,!1,Z)})()}else{f[g]=E;var C=f[m];if(f[m]=s,f[B]===B&&E===$&&(f[g]=f[V],f[m]=f[j]),E===I&&s instanceof Error){var L=r.currentTask&&r.currentTask.data&&r.currentTask.data.__creationTrace__;L&&u(s,O,{configurable:!0,enumerable:!1,writable:!0,value:L})}for(var x=0;x1?new c(T,d):new c(T),w=e.ObjectGetOwnPropertyDescriptor(p,"onmessage");return w&&!1===w.configurable?(y=e.ObjectCreate(p),b=p,[n,u,"send","close"].forEach(function(N){y[N]=function(){var M=e.ArraySlice.call(arguments);if(N===n||N===u){var X=M.length>0?M[0]:void 0;if(X){var g=Zone.__symbol__("ON_PROPERTY"+X);p[g]=y[g]}}return p[N].apply(p,M)}})):y=p,e.patchOnProperties(y,["close","error","message","open"],b),y};var l=r.WebSocket;for(var v in c)l[v]=c[v]}(e,r),Zone[e.symbol("patchEvents")]=!0}}Zone.__load_patch("util",function(e,r,t){t.patchOnProperties=ir,t.patchMethod=ve,t.bindArguments=Xe,t.patchMacroTask=Cr;var n=r.__symbol__("BLACK_LISTED_EVENTS"),u=r.__symbol__("UNPATCHED_EVENTS");e[u]&&(e[n]=e[u]),e[n]&&(r[n]=r[u]=e[n]),t.patchEventPrototype=Mr,t.patchEventTarget=Lr,t.isIEOrEdge=Zr,t.ObjectDefineProperty=xe,t.ObjectGetOwnPropertyDescriptor=we,t.ObjectCreate=Pr,t.ArraySlice=Or,t.patchClass=Re,t.wrapWithCurrentZone=ze,t.filterProperties=_r,t.attachOriginToPatched=he,t._redefineProperty=Object.defineProperty,t.patchCallbacks=Ir,t.getGlobalObjects=function(){return{globalSources:ur,zoneSymbolEventNames:ae,eventNames:ge,isBrowser:qe,isMix:nr,isNode:Ne,TRUE_STR:fe,FALSE_STR:le,ZONE_SYMBOL_PREFIX:Se,ADD_EVENT_LISTENER_STR:Fe,REMOVE_EVENT_LISTENER_STR:Ge}}}),e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},r=e.__Zone_symbol_prefix||"__zone_symbol__",e[function t(n){return r+n}("legacyPatch")]=function(){var n=e.Zone;n.__load_patch("defineProperty",function(u,c,l){l._redefineProperty=Yr,function qr(){Ie=Zone.__symbol__,Ae=Object[Ie("defineProperty")]=Object.defineProperty,pr=Object[Ie("getOwnPropertyDescriptor")]=Object.getOwnPropertyDescriptor,Er=Object.create,pe=Ie("unconfigurables"),Object.defineProperty=function(e,r,t){if(yr(e,r))throw new TypeError("Cannot assign to read only property '"+r+"' of "+e);var n=t.configurable;return"prototype"!==r&&(t=Qe(e,r,t)),mr(e,r,t,n)},Object.defineProperties=function(e,r){return Object.keys(r).forEach(function(t){Object.defineProperty(e,t,r[t])}),e},Object.create=function(e,r){return"object"==typeof r&&!Object.isFrozen(r)&&Object.keys(r).forEach(function(t){r[t]=Qe(e,t,r[t])}),Er(e,r)},Object.getOwnPropertyDescriptor=function(e,r){var t=pr(e,r);return t&&yr(e,r)&&(t.configurable=!1),t}}()}),n.__load_patch("registerElement",function(u,c,l){!function rt(e,r){var t=r.getGlobalObjects();(t.isBrowser||t.isMix)&&"registerElement"in e.document&&r.patchCallbacks(r,document,"Document","registerElement",["createdCallback","attachedCallback","detachedCallback","attributeChangedCallback"])}(u,l)}),n.__load_patch("EventTargetLegacy",function(u,c,l){(function Kr(e,r){var t=r.getGlobalObjects(),n=t.eventNames,u=t.globalSources,c=t.zoneSymbolEventNames,l=t.TRUE_STR,v=t.FALSE_STR,T=t.ZONE_SYMBOL_PREFIX,p="ApplicationCache,EventSource,FileReader,InputMethodContext,MediaController,MessagePort,Node,Performance,SVGElementInstance,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebKitNamedFlow,Window,Worker,WorkerGlobalScope,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload,IDBRequest,IDBOpenDBRequest,IDBDatabase,IDBTransaction,IDBCursor,DBIndex,WebSocket".split(","),y="EventTarget",b=[],w=e.wtf,N="Anchor,Area,Audio,BR,Base,BaseFont,Body,Button,Canvas,Content,DList,Directory,Div,Embed,FieldSet,Font,Form,Frame,FrameSet,HR,Head,Heading,Html,IFrame,Image,Input,Keygen,LI,Label,Legend,Link,Map,Marquee,Media,Menu,Meta,Meter,Mod,OList,Object,OptGroup,Option,Output,Paragraph,Pre,Progress,Quote,Script,Select,Source,Span,Style,TableCaption,TableCell,TableCol,Table,TableRow,TableSection,TextArea,Title,Track,UList,Unknown,Video".split(",");w?b=N.map(function(H){return"HTML"+H+"Element"}).concat(p):e[y]?b.push(y):b=p;for(var M=e.__Zone_disable_IE_check||!1,X=e.__Zone_enable_cross_context_check||!1,g=r.isIEOrEdge(),B="[object FunctionWrapper]",j="function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }",V={MSPointerCancel:"pointercancel",MSPointerDown:"pointerdown",MSPointerEnter:"pointerenter",MSPointerHover:"pointerhover",MSPointerLeave:"pointerleave",MSPointerMove:"pointermove",MSPointerOut:"pointerout",MSPointerOver:"pointerover",MSPointerUp:"pointerup"},U=0;U0){var h=R.invoke;R.invoke=function(){for(var a=O[r.__symbol__("loadfalse")],o=0;o{Ee(Ee.s=435)}]); --------------------------------------------------------------------------------