├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .vscode └── tasks.json ├── CODEOWNERS ├── README.md ├── bin ├── account-vitals ├── customer-vitals ├── facade-vitals ├── get-account ├── get-account-summary └── transaction-vitals ├── doc ├── app-arch.graffle │ ├── data.plist │ └── image1.pdf ├── app-arch.png ├── app-mock.png ├── domain.png ├── facade.png ├── get-account-summary.png ├── health.png ├── hi-res │ └── facade │ │ ├── Account Summaryxl.png │ │ ├── App mockxl.png │ │ ├── Application Architecture 2xl.png │ │ ├── Application Architecturexl.png │ │ ├── Content @2x 2xl.png │ │ ├── Healthxl.png │ │ ├── Request : Responsexl.png │ │ └── Topologyxl.png ├── hi-resolution │ ├── Account Summary.png │ ├── App mock.png │ ├── Application Architecture.png │ ├── Content @2x 2.png │ ├── Facade Pattern.png │ ├── Health.png │ ├── Request : Response.png │ ├── Topology.png │ ├── aliveness.png │ ├── health-check-impl.png │ ├── health-simple.png │ ├── hi-level-app-arch.png │ ├── loopback-apps.png │ ├── polyglot.png │ └── soa-arch.png └── request-caching.png ├── docker-compose.yml ├── health-check-client └── index.js ├── package.json ├── services ├── account │ ├── Dockerfile │ ├── common │ │ └── models │ │ │ ├── account.js │ │ │ ├── account.json │ │ │ ├── vital.js │ │ │ └── vital.json │ ├── data.json │ ├── package.json │ ├── server │ │ ├── boot │ │ │ └── root.js │ │ ├── component-config.json │ │ ├── config.json │ │ ├── datasources.json │ │ ├── middleware.development.json │ │ ├── middleware.json │ │ ├── model-config.json │ │ └── server.js │ └── test.yml ├── customer │ ├── Dockerfile │ ├── common │ │ └── models │ │ │ ├── address.js │ │ │ ├── address.json │ │ │ ├── customer.js │ │ │ ├── customer.json │ │ │ ├── vital.js │ │ │ └── vital.json │ ├── data.json │ ├── package.json │ ├── server │ │ ├── boot │ │ │ └── root.js │ │ ├── component-config.json │ │ ├── config.json │ │ ├── datasources.json │ │ ├── middleware.development.json │ │ ├── middleware.json │ │ ├── model-config.json │ │ └── server.js │ └── test.yml ├── facade │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc │ ├── .yo-rc.json │ ├── Dockerfile │ ├── common │ │ └── models │ │ │ ├── account-cache.js │ │ │ ├── account-cache.json │ │ │ ├── account.js │ │ │ ├── account.json │ │ │ ├── cache.js │ │ │ ├── cache.json │ │ │ ├── customer-cache.js │ │ │ ├── customer-cache.json │ │ │ ├── customer.js │ │ │ ├── customer.json │ │ │ ├── transaction-cache.js │ │ │ ├── transaction-cache.json │ │ │ ├── transaction.js │ │ │ ├── transaction.json │ │ │ ├── vital.js │ │ │ └── vital.json │ ├── package.json │ └── server │ │ ├── boot │ │ └── root.js │ │ ├── component-config.json │ │ ├── config.json │ │ ├── datasources.json │ │ ├── middleware.development.json │ │ ├── middleware.json │ │ ├── model-config.json │ │ └── server.js └── transaction │ ├── Dockerfile │ ├── common │ └── models │ │ ├── cheque.js │ │ ├── cheque.json │ │ ├── transaction.js │ │ ├── transaction.json │ │ ├── vital.js │ │ └── vital.json │ ├── data.json │ ├── package.json │ └── server │ ├── boot │ └── root.js │ ├── component-config.json │ ├── config.json │ ├── datasources.json │ ├── middleware.development.json │ ├── middleware.json │ ├── model-config.json │ └── server.js ├── spec └── facade.yml └── test ├── acceptance └── facade │ ├── account-summary.test.js │ ├── index.js │ └── vitals.test.js ├── mocha.opts └── support └── expect.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # Description/Steps to reproduce 11 | 12 | 16 | 17 | # Link to reproduction sandbox 18 | 19 | 24 | 25 | # Expected result 26 | 27 | 30 | 31 | # Additional information 32 | 33 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - connect to 14 | 15 | ### Checklist 16 | 17 | 22 | 23 | - [ ] New tests added or existing tests modified to cover all changes 24 | - [ ] Code conforms with the [style 25 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "always", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "install", 12 | "args": ["install"] 13 | }, 14 | { 15 | "taskName": "update", 16 | "args": ["update"] 17 | }, 18 | { 19 | "taskName": "test", 20 | "args": ["run", "test"] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precedence. 4 | 5 | # Core team members from IBM 6 | * @bajtos @superkhau 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-example-facade 2 | 3 | **⚠️ This LoopBack 3 example project is no longer maintained. Please refer to [LoopBack 4 Examples](https://loopback.io/doc/en/lb4/Examples.html) instead. ⚠️** 4 | 5 | This example demonstrates best practices for building scalable Microservices. [See the slides](https://www.slideshare.net/RitchieMartori/scaling-your-microservices-with-loopback) for an overview. 6 | 7 | - [The Interaction Tier](https://github.com/strongloop/loopback-example-facade/wiki/The-Interaction-Tier) 8 | - [The Microservice Facade Pattern](https://github.com/strongloop/loopback-example-facade/wiki/The-Microservice-Facade-Pattern) 9 | - [Caching Strategies](https://github.com/strongloop/loopback-example-facade/wiki/Caching-Strategies) 10 | - [Health Checks and System Status](https://github.com/strongloop/loopback-example-facade/wiki/Health-Checks-and-System-Status) 11 | - [Transforming your Architecture](https://github.com/strongloop/loopback-example-facade/wiki/Transforming-your-Architecture) 12 | 13 | 14 | 15 | ## Try it out 16 | 17 | **Download and run the code.** 18 | 19 | ``` 20 | $ git clone https://github.com/strongloop/loopback-example-facade 21 | $ cd loopback-example-facade 22 | $ docker-compose up --build 23 | ``` 24 | 25 | **Make a request to get the account summary screen data.** 26 | 27 | ``` 28 | $ bin/get-account-summary 29 | ``` 30 | 31 | **Run the System Status Health Check** 32 | 33 | ``` 34 | $ node health-check-client 35 | ``` 36 | -------------------------------------------------------------------------------- /bin/account-vitals: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s http://localhost:3002/vitals | jq 4 | -------------------------------------------------------------------------------- /bin/customer-vitals: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s http://localhost:3001/vitals | jq 4 | -------------------------------------------------------------------------------- /bin/facade-vitals: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s http://localhost:3000/vitals | jq 4 | -------------------------------------------------------------------------------- /bin/get-account: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s "http://localhost:3000/api/Accounts/CHK52321122" | jq 4 | -------------------------------------------------------------------------------- /bin/get-account-summary: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s "http://localhost:3000/api/Accounts/summary?accountNumber=CHK52321122&cache=$1" | jq 4 | -------------------------------------------------------------------------------- /bin/transaction-vitals: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | time curl -s http://localhost:3003/vitals | jq 4 | -------------------------------------------------------------------------------- /doc/app-arch.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/app-arch.graffle/data.plist -------------------------------------------------------------------------------- /doc/app-arch.graffle/image1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/app-arch.graffle/image1.pdf -------------------------------------------------------------------------------- /doc/app-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/app-arch.png -------------------------------------------------------------------------------- /doc/app-mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/app-mock.png -------------------------------------------------------------------------------- /doc/domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/domain.png -------------------------------------------------------------------------------- /doc/facade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/facade.png -------------------------------------------------------------------------------- /doc/get-account-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/get-account-summary.png -------------------------------------------------------------------------------- /doc/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/health.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Account Summaryxl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Account Summaryxl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/App mockxl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/App mockxl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Application Architecture 2xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Application Architecture 2xl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Application Architecturexl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Application Architecturexl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Content @2x 2xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Content @2x 2xl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Healthxl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Healthxl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Request : Responsexl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Request : Responsexl.png -------------------------------------------------------------------------------- /doc/hi-res/facade/Topologyxl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-res/facade/Topologyxl.png -------------------------------------------------------------------------------- /doc/hi-resolution/Account Summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Account Summary.png -------------------------------------------------------------------------------- /doc/hi-resolution/App mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/App mock.png -------------------------------------------------------------------------------- /doc/hi-resolution/Application Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Application Architecture.png -------------------------------------------------------------------------------- /doc/hi-resolution/Content @2x 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Content @2x 2.png -------------------------------------------------------------------------------- /doc/hi-resolution/Facade Pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Facade Pattern.png -------------------------------------------------------------------------------- /doc/hi-resolution/Health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Health.png -------------------------------------------------------------------------------- /doc/hi-resolution/Request : Response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Request : Response.png -------------------------------------------------------------------------------- /doc/hi-resolution/Topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/Topology.png -------------------------------------------------------------------------------- /doc/hi-resolution/aliveness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/aliveness.png -------------------------------------------------------------------------------- /doc/hi-resolution/health-check-impl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/health-check-impl.png -------------------------------------------------------------------------------- /doc/hi-resolution/health-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/health-simple.png -------------------------------------------------------------------------------- /doc/hi-resolution/hi-level-app-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/hi-level-app-arch.png -------------------------------------------------------------------------------- /doc/hi-resolution/loopback-apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/loopback-apps.png -------------------------------------------------------------------------------- /doc/hi-resolution/polyglot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/polyglot.png -------------------------------------------------------------------------------- /doc/hi-resolution/soa-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/hi-resolution/soa-arch.png -------------------------------------------------------------------------------- /doc/request-caching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-example-facade/e8f25ef2661b36789229bff095071cbdddc9a484/doc/request-caching.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | facade: 4 | build: ./services/facade 5 | image: loopback-example-facade/facade 6 | ports: 7 | - "3000:80" 8 | links: 9 | - account 10 | - customer 11 | - transaction 12 | customer: 13 | build: ./services/customer 14 | image: loopback-example-facade/customer-service 15 | ports: 16 | - "3001:80" 17 | account: 18 | build: ./services/account 19 | image: loopback-example-facade/account-service 20 | ports: 21 | - "3002:80" 22 | transaction: 23 | build: ./services/transaction 24 | image: loopback-example-facade/transaction 25 | ports: 26 | - "3003:80" 27 | mongo: 28 | image: mongo 29 | volumes: 30 | - /usr/local/var/mongodb:/data/db 31 | ports: 32 | - "27017:27017" 33 | -------------------------------------------------------------------------------- /health-check-client/index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | var colors = require('colors'); 5 | var http = require('http'); 6 | 7 | checkHealth('localhost', 3000, '/api/Vitals'); 8 | 9 | function checkHealth(host, port, path) { 10 | var httpOptions = { 11 | host: host, 12 | port: port, 13 | path: path, 14 | method: 'GET' 15 | }; 16 | var req = http.request(httpOptions, function(response) { 17 | var str = ''; 18 | response.on('data', function(chunk) { 19 | str = str + chunk; 20 | }); 21 | response.on('end', function() { 22 | printHealth(JSON.parse(str)); 23 | }); 24 | }); 25 | req.on('error', function(err) { 26 | console.log(err); 27 | }); 28 | req.end(); 29 | } 30 | 31 | const HPIPE = '──'; 32 | const BPIPE = '└'; 33 | const TPIPE = '├'; 34 | const COLORS = { 35 | healthy: 'green', 36 | unhealthy: 'red' 37 | }; 38 | 39 | function printHealth(vitals) { 40 | console.log(` ├── ${'facade'.white} (${vitals.status.green})`); 41 | printDependencies(vitals.dependencies); 42 | } 43 | 44 | function printDependencies(dependencies, depth) { 45 | Object.keys(dependencies).forEach(function(name) { 46 | let info = dependencies[name]; 47 | let prefix = ''; 48 | let status = info.status[COLORS[info.status]]; 49 | depth = depth || 1; 50 | 51 | for(let i = 0; i < depth; i++) { 52 | prefix = '| ' + prefix; 53 | } 54 | 55 | if (depth === 1) { 56 | prefix += '└──' 57 | } else { 58 | prefix += '├──' 59 | } 60 | let latency; 61 | if (info.latency) { 62 | latency = info.latency.toString(); 63 | } 64 | if(latency) { 65 | console.log(` ${prefix} ${name.white} (${status}) ${'latency'.yellow}: ${latency.green}`); 66 | } else { 67 | console.log(` ${prefix} ${name.white} (${status})`); 68 | } 69 | if (info.dependencies) printDependencies(info.dependencies, depth + 1); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-facade", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/strongloop/loopback-example-facade.git" 12 | }, 13 | "author": "IBM Corp.", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/strongloop/loopback-example-facade/issues" 17 | }, 18 | "homepage": "https://github.com/strongloop/loopback-example-facade#readme", 19 | "dependencies": { 20 | "colors": "^1.1.2" 21 | }, 22 | "devDependencies": { 23 | "chai": "^3.5.0", 24 | "chai-subset": "^1.5.0", 25 | "debug": "^2.6.3", 26 | "dirty-chai": "^1.2.2", 27 | "mocha": "^3.2.0", 28 | "request": "^2.81.0", 29 | "request-promise": "^4.1.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/account/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/account-service 5 | WORKDIR /usr/src/account-service 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/account-service 9 | RUN npm install 10 | 11 | # Bundle app source 12 | COPY . /usr/src/account-service 13 | 14 | EXPOSE 3000 15 | 16 | HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://localhost/vitals/docker || exit 1 17 | 18 | CMD [ "node", "." ] 19 | -------------------------------------------------------------------------------- /services/account/common/models/account.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | module.exports = function(Account) { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /services/account/common/models/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account", 3 | "idInjection": false, 4 | "options": { 5 | "validateUpsert": true 6 | }, 7 | "properties": { 8 | "accountNumber": { 9 | "type": "string", 10 | "id": true, 11 | "required": true 12 | }, 13 | "customerNumber": { 14 | "type": "string", 15 | "required": true 16 | }, 17 | "balance": { 18 | "type": "number" 19 | }, 20 | "branch": { 21 | "type": "string" 22 | }, 23 | "type": { 24 | "type": "string" 25 | }, 26 | "avgBalance": { 27 | "type": "number" 28 | }, 29 | "minimumBalance": { 30 | "type": "number" 31 | } 32 | }, 33 | "validations": [], 34 | "relations": {}, 35 | "acls": [], 36 | "methods": {} 37 | } 38 | -------------------------------------------------------------------------------- /services/account/common/models/vital.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const app = require('../../server/server'); 5 | const Promise = require('bluebird'); 6 | 7 | module.exports = function(Vital) { 8 | Vital.check = async function() { 9 | const status = { 10 | status: 'healthy', 11 | dependencies: {}, 12 | } 13 | const dsList = app.dataSources; 14 | const dsNames = Object.keys(dsList); 15 | dsNames.forEach(function(dsName) { 16 | status.dependencies[dsName] = checkVital(dsList[dsName]); 17 | }); 18 | status.dependencies = await Promise.props(status.dependencies); 19 | return status; 20 | }; 21 | function checkVital(ds) { 22 | return new Promise(function(resolve, reject) { 23 | ds.ping(function(err, result) { 24 | if (err) return reject({status: 'unhealthy'}); 25 | resolve({status: 'healthy'}); 26 | }); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /services/account/common/models/vital.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vital", 3 | "idInjection": false, 4 | "base": "Model", 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": { 13 | "check": { 14 | "returns": [ 15 | { 16 | "arg": "vitals", 17 | "type": "Object", 18 | "root": true, 19 | "description": "health check" 20 | } 21 | ], 22 | "description": "do health check", 23 | "http": [ 24 | { 25 | "path": "/", 26 | "verb": "get" 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/account/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "Account": 4 4 | }, 5 | "models": { 6 | "Account": { 7 | "CHK52321122": "{\"accountNumber\":\"CHK52321122\",\"customerNumber\":\"000343223\",\"balance\":85.84,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":398.93,\"minimumBalance\":10}", 8 | "CHK54520000": "{\"accountNumber\":\"CHK54520000\",\"customerNumber\":\"003499223\",\"balance\":99.99,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":500.93,\"minimumBalance\":10}", 9 | "CHK52321199": "{\"accountNumber\":\"CHK52321199\",\"customerNumber\":\"000343223\",\"balance\":109.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}", 10 | "CHK99999999": "{\"accountNumber\":\"CHK99999999\",\"customerNumber\":\"0002444422\",\"balance\":100.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /services/account/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "start": "node .", 8 | "posttest": "npm run lint && nsp check" 9 | }, 10 | "dependencies": { 11 | "compression": "^1.0.3", 12 | "cors": "^2.5.2", 13 | "helmet": "^1.3.0", 14 | "loopback": "^3.0.0", 15 | "loopback-boot": "^2.6.5", 16 | "loopback-component-explorer": "^4.0.0", 17 | "morgan": "^1.7.0", 18 | "serve-favicon": "^2.0.1", 19 | "strong-error-handler": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^2.13.1", 23 | "eslint-config-loopback": "^4.0.0", 24 | "nsp": "^2.1.0" 25 | }, 26 | "repository": { 27 | "type": "", 28 | "url": "" 29 | }, 30 | "license": "UNLICENSED", 31 | "description": "account" 32 | } 33 | -------------------------------------------------------------------------------- /services/account/server/boot/root.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(server) { 7 | // Install a `/` route that returns server status 8 | var router = server.loopback.Router(); 9 | router.get('/', server.loopback.status()); 10 | server.use(router); 11 | }; 12 | -------------------------------------------------------------------------------- /services/account/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/account/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 80, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "handleErrors": false, 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/account/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AccountDB": { 3 | "name": "AccountDB", 4 | "connector": "memory", 5 | "file": "data.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /services/account/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/account/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {}, 4 | "morgan": { 5 | "params": ["dev"] 6 | } 7 | }, 8 | "initial": { 9 | "compression": {}, 10 | "cors": { 11 | "params": { 12 | "origin": true, 13 | "credentials": true, 14 | "maxAge": 86400 15 | } 16 | }, 17 | "helmet#xssFilter": {}, 18 | "helmet#frameguard": { 19 | "params": [ 20 | "deny" 21 | ] 22 | }, 23 | "helmet#hsts": { 24 | "params": { 25 | "maxAge": 0, 26 | "includeSubdomains": true 27 | } 28 | }, 29 | "helmet#hidePoweredBy": {}, 30 | "helmet#ieNoOpen": {}, 31 | "helmet#noSniff": {}, 32 | "helmet#noCache": { 33 | "enabled": false 34 | } 35 | }, 36 | "session": {}, 37 | "auth": {}, 38 | "parse": {}, 39 | "routes": { 40 | "loopback#rest": { 41 | "paths": [ 42 | "${restApiRoot}" 43 | ] 44 | } 45 | }, 46 | "files": {}, 47 | "final": { 48 | "loopback#urlNotFound": {} 49 | }, 50 | "final:after": { 51 | "strong-error-handler": {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /services/account/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "Account": { 17 | "dataSource": "AccountDB", 18 | "public": true 19 | }, 20 | "Vital": { 21 | "dataSource": null, 22 | "public": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/account/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | var loopback = require('loopback'); 7 | var boot = require('loopback-boot'); 8 | 9 | var app = module.exports = loopback(); 10 | 11 | // intentional delay to simulate slow microservice 12 | app.use((req, res, next) => { 13 | setTimeout(next, 3000); 14 | }); 15 | 16 | app.get('/vitals/docker', (req, res) => { 17 | res.send('ok'); 18 | }); 19 | 20 | app.start = function() { 21 | // start the web server 22 | return app.listen(function() { 23 | app.emit('started'); 24 | var baseUrl = app.get('url').replace(/\/$/, ''); 25 | console.log('Web server listening at: %s', baseUrl); 26 | if (app.get('loopback-component-explorer')) { 27 | var explorerPath = app.get('loopback-component-explorer').mountPath; 28 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 29 | } 30 | }); 31 | }; 32 | 33 | // Bootstrap the application, configure models, datasources and middleware. 34 | // Sub-apps like REST API are mounted via boot scripts. 35 | boot(app, __dirname, function(err) { 36 | if (err) throw err; 37 | 38 | // start the server if `$ node server.js` 39 | if (require.main === module) 40 | app.start(); 41 | }); 42 | -------------------------------------------------------------------------------- /services/account/test.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: 1.0.0 4 | title: account 5 | basePath: /api 6 | consumes: 7 | - application/json 8 | - application/x-www-form-urlencoded 9 | - application/xml 10 | - text/xml 11 | produces: 12 | - application/json 13 | - application/xml 14 | - text/xml 15 | - application/javascript 16 | - text/javascript 17 | paths: 18 | /Accounts: 19 | post: 20 | tags: 21 | - Account 22 | summary: Create a new instance of the model and persist it into the data source. 23 | operationId: Account.create 24 | parameters: 25 | - name: data 26 | in: body 27 | description: Model instance data 28 | required: false 29 | schema: 30 | $ref: '#/definitions/Account' 31 | responses: 32 | '200': 33 | description: Request was successful 34 | schema: 35 | $ref: '#/definitions/Account' 36 | deprecated: false 37 | patch: 38 | tags: 39 | - Account 40 | summary: >- 41 | Patch an existing model instance or insert a new one into the data 42 | source. 43 | operationId: Account.patchOrCreate 44 | parameters: 45 | - name: data 46 | in: body 47 | description: Model instance data 48 | required: false 49 | schema: 50 | $ref: '#/definitions/Account' 51 | responses: 52 | '200': 53 | description: Request was successful 54 | schema: 55 | $ref: '#/definitions/Account' 56 | deprecated: false 57 | put: 58 | tags: 59 | - Account 60 | summary: >- 61 | Replace an existing model instance or insert a new one into the data 62 | source. 63 | operationId: Account.replaceOrCreate__put_Accounts 64 | parameters: 65 | - name: data 66 | in: body 67 | description: Model instance data 68 | required: false 69 | schema: 70 | $ref: '#/definitions/Account' 71 | responses: 72 | '200': 73 | description: Request was successful 74 | schema: 75 | $ref: '#/definitions/Account' 76 | deprecated: false 77 | get: 78 | tags: 79 | - Account 80 | summary: Find all instances of the model matched by filter from the data source. 81 | operationId: Account.find 82 | parameters: 83 | - name: filter 84 | in: query 85 | description: >- 86 | Filter defining fields, where, include, order, offset, and limit - 87 | must be a JSON-encoded string ({"something":"value"}) 88 | required: false 89 | type: string 90 | format: JSON 91 | responses: 92 | '200': 93 | description: Request was successful 94 | schema: 95 | type: array 96 | items: 97 | $ref: '#/definitions/Account' 98 | deprecated: false 99 | /Accounts/replaceOrCreate: 100 | post: 101 | tags: 102 | - Account 103 | summary: >- 104 | Replace an existing model instance or insert a new one into the data 105 | source. 106 | operationId: Account.replaceOrCreate__post_Accounts_replaceOrCreate 107 | parameters: 108 | - name: data 109 | in: body 110 | description: Model instance data 111 | required: false 112 | schema: 113 | $ref: '#/definitions/Account' 114 | responses: 115 | '200': 116 | description: Request was successful 117 | schema: 118 | $ref: '#/definitions/Account' 119 | deprecated: false 120 | /Accounts/upsertWithWhere: 121 | post: 122 | tags: 123 | - Account 124 | summary: >- 125 | Update an existing model instance or insert a new one into the data 126 | source based on the where criteria. 127 | operationId: Account.upsertWithWhere 128 | parameters: 129 | - name: where 130 | in: query 131 | description: Criteria to match model instances 132 | required: false 133 | type: string 134 | format: JSON 135 | - name: data 136 | in: body 137 | description: An object of model property name/value pairs 138 | required: false 139 | schema: 140 | $ref: '#/definitions/Account' 141 | responses: 142 | '200': 143 | description: Request was successful 144 | schema: 145 | $ref: '#/definitions/Account' 146 | deprecated: false 147 | '/Accounts/{id}/exists': 148 | get: 149 | tags: 150 | - Account 151 | summary: Check whether a model instance exists in the data source. 152 | operationId: 'Account.exists__get_Accounts_{id}_exists' 153 | parameters: 154 | - name: id 155 | in: path 156 | description: Model id 157 | required: true 158 | type: string 159 | format: JSON 160 | responses: 161 | '200': 162 | description: Request was successful 163 | schema: 164 | type: object 165 | properties: 166 | exists: 167 | type: boolean 168 | deprecated: false 169 | '/Accounts/{id}': 170 | head: 171 | tags: 172 | - Account 173 | summary: Check whether a model instance exists in the data source. 174 | operationId: 'Account.exists__head_Accounts_{id}' 175 | parameters: 176 | - name: id 177 | in: path 178 | description: Model id 179 | required: true 180 | type: string 181 | format: JSON 182 | responses: 183 | '200': 184 | description: Request was successful 185 | schema: 186 | type: object 187 | properties: 188 | exists: 189 | type: boolean 190 | deprecated: false 191 | get: 192 | tags: 193 | - Account 194 | summary: 'Find a model instance by {{id}} from the data source.' 195 | operationId: Account.findById 196 | parameters: 197 | - name: id 198 | in: path 199 | description: Model id 200 | required: true 201 | type: string 202 | format: JSON 203 | - name: filter 204 | in: query 205 | description: >- 206 | Filter defining fields and include - must be a JSON-encoded string 207 | ({"something":"value"}) 208 | required: false 209 | type: string 210 | format: JSON 211 | responses: 212 | '200': 213 | description: Request was successful 214 | schema: 215 | $ref: '#/definitions/Account' 216 | deprecated: false 217 | put: 218 | tags: 219 | - Account 220 | summary: >- 221 | Replace attributes for a model instance and persist it into the data 222 | source. 223 | operationId: 'Account.replaceById__put_Accounts_{id}' 224 | parameters: 225 | - name: id 226 | in: path 227 | description: Model id 228 | required: true 229 | type: string 230 | format: JSON 231 | - name: data 232 | in: body 233 | description: Model instance data 234 | required: false 235 | schema: 236 | $ref: '#/definitions/Account' 237 | responses: 238 | '200': 239 | description: Request was successful 240 | schema: 241 | $ref: '#/definitions/Account' 242 | deprecated: false 243 | delete: 244 | tags: 245 | - Account 246 | summary: 'Delete a model instance by {{id}} from the data source.' 247 | operationId: Account.deleteById 248 | parameters: 249 | - name: id 250 | in: path 251 | description: Model id 252 | required: true 253 | type: string 254 | format: JSON 255 | responses: 256 | '200': 257 | description: Request was successful 258 | schema: 259 | type: object 260 | deprecated: false 261 | patch: 262 | tags: 263 | - Account 264 | summary: >- 265 | Patch attributes for a model instance and persist it into the data 266 | source. 267 | operationId: Account.prototype.patchAttributes 268 | parameters: 269 | - name: data 270 | in: body 271 | description: An object of model property name/value pairs 272 | required: false 273 | schema: 274 | $ref: '#/definitions/Account' 275 | - name: id 276 | in: path 277 | description: Account id 278 | required: true 279 | type: string 280 | format: JSON 281 | responses: 282 | '200': 283 | description: Request was successful 284 | schema: 285 | $ref: '#/definitions/Account' 286 | deprecated: false 287 | '/Accounts/{id}/replace': 288 | post: 289 | tags: 290 | - Account 291 | summary: >- 292 | Replace attributes for a model instance and persist it into the data 293 | source. 294 | operationId: 'Account.replaceById__post_Accounts_{id}_replace' 295 | parameters: 296 | - name: id 297 | in: path 298 | description: Model id 299 | required: true 300 | type: string 301 | format: JSON 302 | - name: data 303 | in: body 304 | description: Model instance data 305 | required: false 306 | schema: 307 | $ref: '#/definitions/Account' 308 | responses: 309 | '200': 310 | description: Request was successful 311 | schema: 312 | $ref: '#/definitions/Account' 313 | deprecated: false 314 | /Accounts/findOne: 315 | get: 316 | tags: 317 | - Account 318 | summary: Find first instance of the model matched by filter from the data source. 319 | operationId: Account.findOne 320 | parameters: 321 | - name: filter 322 | in: query 323 | description: >- 324 | Filter defining fields, where, include, order, offset, and limit - 325 | must be a JSON-encoded string ({"something":"value"}) 326 | required: false 327 | type: string 328 | format: JSON 329 | responses: 330 | '200': 331 | description: Request was successful 332 | schema: 333 | $ref: '#/definitions/Account' 334 | deprecated: false 335 | /Accounts/update: 336 | post: 337 | tags: 338 | - Account 339 | summary: 'Update instances of the model matched by {{where}} from the data source.' 340 | operationId: Account.updateAll 341 | parameters: 342 | - name: where 343 | in: query 344 | description: Criteria to match model instances 345 | required: false 346 | type: string 347 | format: JSON 348 | - name: data 349 | in: body 350 | description: An object of model property name/value pairs 351 | required: false 352 | schema: 353 | $ref: '#/definitions/Account' 354 | responses: 355 | '200': 356 | description: Request was successful 357 | schema: 358 | description: Information related to the outcome of the operation 359 | type: object 360 | deprecated: false 361 | /Accounts/count: 362 | get: 363 | tags: 364 | - Account 365 | summary: Count instances of the model matched by where from the data source. 366 | operationId: Account.count 367 | parameters: 368 | - name: where 369 | in: query 370 | description: Criteria to match model instances 371 | required: false 372 | type: string 373 | format: JSON 374 | responses: 375 | '200': 376 | description: Request was successful 377 | schema: 378 | type: object 379 | properties: 380 | count: 381 | type: number 382 | format: double 383 | deprecated: false 384 | /Accounts/change-stream: 385 | post: 386 | tags: 387 | - Account 388 | summary: Create a change stream. 389 | operationId: Account.createChangeStream__post_Accounts_change-stream 390 | parameters: 391 | - name: options 392 | in: formData 393 | required: false 394 | type: string 395 | format: JSON 396 | responses: 397 | '200': 398 | description: Request was successful 399 | schema: 400 | type: file 401 | deprecated: false 402 | get: 403 | tags: 404 | - Account 405 | summary: Create a change stream. 406 | operationId: Account.createChangeStream__get_Accounts_change-stream 407 | parameters: 408 | - name: options 409 | in: query 410 | required: false 411 | type: string 412 | format: JSON 413 | responses: 414 | '200': 415 | description: Request was successful 416 | schema: 417 | type: file 418 | deprecated: false 419 | /AccountSummaries: 420 | post: 421 | tags: 422 | - AccountSummary 423 | summary: Create a new instance of the model and persist it into the data source. 424 | operationId: AccountSummary.create 425 | parameters: 426 | - name: data 427 | in: body 428 | description: Model instance data 429 | required: false 430 | schema: 431 | $ref: '#/definitions/AccountSummary' 432 | responses: 433 | '200': 434 | description: Request was successful 435 | schema: 436 | $ref: '#/definitions/AccountSummary' 437 | deprecated: false 438 | patch: 439 | tags: 440 | - AccountSummary 441 | summary: >- 442 | Patch an existing model instance or insert a new one into the data 443 | source. 444 | operationId: AccountSummary.patchOrCreate 445 | parameters: 446 | - name: data 447 | in: body 448 | description: Model instance data 449 | required: false 450 | schema: 451 | $ref: '#/definitions/AccountSummary' 452 | responses: 453 | '200': 454 | description: Request was successful 455 | schema: 456 | $ref: '#/definitions/AccountSummary' 457 | deprecated: false 458 | put: 459 | tags: 460 | - AccountSummary 461 | summary: >- 462 | Replace an existing model instance or insert a new one into the data 463 | source. 464 | operationId: AccountSummary.replaceOrCreate__put_AccountSummaries 465 | parameters: 466 | - name: data 467 | in: body 468 | description: Model instance data 469 | required: false 470 | schema: 471 | $ref: '#/definitions/AccountSummary' 472 | responses: 473 | '200': 474 | description: Request was successful 475 | schema: 476 | $ref: '#/definitions/AccountSummary' 477 | deprecated: false 478 | get: 479 | tags: 480 | - AccountSummary 481 | summary: Find all instances of the model matched by filter from the data source. 482 | operationId: AccountSummary.find 483 | parameters: 484 | - name: filter 485 | in: query 486 | description: >- 487 | Filter defining fields, where, include, order, offset, and limit - 488 | must be a JSON-encoded string ({"something":"value"}) 489 | required: false 490 | type: string 491 | format: JSON 492 | responses: 493 | '200': 494 | description: Request was successful 495 | schema: 496 | type: array 497 | items: 498 | $ref: '#/definitions/AccountSummary' 499 | deprecated: false 500 | /AccountSummaries/replaceOrCreate: 501 | post: 502 | tags: 503 | - AccountSummary 504 | summary: >- 505 | Replace an existing model instance or insert a new one into the data 506 | source. 507 | operationId: AccountSummary.replaceOrCreate__post_AccountSummaries_replaceOrCreate 508 | parameters: 509 | - name: data 510 | in: body 511 | description: Model instance data 512 | required: false 513 | schema: 514 | $ref: '#/definitions/AccountSummary' 515 | responses: 516 | '200': 517 | description: Request was successful 518 | schema: 519 | $ref: '#/definitions/AccountSummary' 520 | deprecated: false 521 | /AccountSummaries/upsertWithWhere: 522 | post: 523 | tags: 524 | - AccountSummary 525 | summary: >- 526 | Update an existing model instance or insert a new one into the data 527 | source based on the where criteria. 528 | operationId: AccountSummary.upsertWithWhere 529 | parameters: 530 | - name: where 531 | in: query 532 | description: Criteria to match model instances 533 | required: false 534 | type: string 535 | format: JSON 536 | - name: data 537 | in: body 538 | description: An object of model property name/value pairs 539 | required: false 540 | schema: 541 | $ref: '#/definitions/AccountSummary' 542 | responses: 543 | '200': 544 | description: Request was successful 545 | schema: 546 | $ref: '#/definitions/AccountSummary' 547 | deprecated: false 548 | '/AccountSummaries/{id}/exists': 549 | get: 550 | tags: 551 | - AccountSummary 552 | summary: Check whether a model instance exists in the data source. 553 | operationId: 'AccountSummary.exists__get_AccountSummaries_{id}_exists' 554 | parameters: 555 | - name: id 556 | in: path 557 | description: Model id 558 | required: true 559 | type: string 560 | format: JSON 561 | responses: 562 | '200': 563 | description: Request was successful 564 | schema: 565 | type: object 566 | properties: 567 | exists: 568 | type: boolean 569 | deprecated: false 570 | '/AccountSummaries/{id}': 571 | head: 572 | tags: 573 | - AccountSummary 574 | summary: Check whether a model instance exists in the data source. 575 | operationId: 'AccountSummary.exists__head_AccountSummaries_{id}' 576 | parameters: 577 | - name: id 578 | in: path 579 | description: Model id 580 | required: true 581 | type: string 582 | format: JSON 583 | responses: 584 | '200': 585 | description: Request was successful 586 | schema: 587 | type: object 588 | properties: 589 | exists: 590 | type: boolean 591 | deprecated: false 592 | get: 593 | tags: 594 | - AccountSummary 595 | summary: 'Find a model instance by {{id}} from the data source.' 596 | operationId: AccountSummary.findById 597 | parameters: 598 | - name: id 599 | in: path 600 | description: Model id 601 | required: true 602 | type: string 603 | format: JSON 604 | - name: filter 605 | in: query 606 | description: >- 607 | Filter defining fields and include - must be a JSON-encoded string 608 | ({"something":"value"}) 609 | required: false 610 | type: string 611 | format: JSON 612 | responses: 613 | '200': 614 | description: Request was successful 615 | schema: 616 | $ref: '#/definitions/AccountSummary' 617 | deprecated: false 618 | put: 619 | tags: 620 | - AccountSummary 621 | summary: >- 622 | Replace attributes for a model instance and persist it into the data 623 | source. 624 | operationId: 'AccountSummary.replaceById__put_AccountSummaries_{id}' 625 | parameters: 626 | - name: id 627 | in: path 628 | description: Model id 629 | required: true 630 | type: string 631 | format: JSON 632 | - name: data 633 | in: body 634 | description: Model instance data 635 | required: false 636 | schema: 637 | $ref: '#/definitions/AccountSummary' 638 | responses: 639 | '200': 640 | description: Request was successful 641 | schema: 642 | $ref: '#/definitions/AccountSummary' 643 | deprecated: false 644 | delete: 645 | tags: 646 | - AccountSummary 647 | summary: 'Delete a model instance by {{id}} from the data source.' 648 | operationId: AccountSummary.deleteById 649 | parameters: 650 | - name: id 651 | in: path 652 | description: Model id 653 | required: true 654 | type: string 655 | format: JSON 656 | responses: 657 | '200': 658 | description: Request was successful 659 | schema: 660 | type: object 661 | deprecated: false 662 | patch: 663 | tags: 664 | - AccountSummary 665 | summary: >- 666 | Patch attributes for a model instance and persist it into the data 667 | source. 668 | operationId: AccountSummary.prototype.patchAttributes 669 | parameters: 670 | - name: data 671 | in: body 672 | description: An object of model property name/value pairs 673 | required: false 674 | schema: 675 | $ref: '#/definitions/AccountSummary' 676 | - name: id 677 | in: path 678 | description: AccountSummary id 679 | required: true 680 | type: string 681 | format: JSON 682 | responses: 683 | '200': 684 | description: Request was successful 685 | schema: 686 | $ref: '#/definitions/AccountSummary' 687 | deprecated: false 688 | '/AccountSummaries/{id}/replace': 689 | post: 690 | tags: 691 | - AccountSummary 692 | summary: >- 693 | Replace attributes for a model instance and persist it into the data 694 | source. 695 | operationId: 'AccountSummary.replaceById__post_AccountSummaries_{id}_replace' 696 | parameters: 697 | - name: id 698 | in: path 699 | description: Model id 700 | required: true 701 | type: string 702 | format: JSON 703 | - name: data 704 | in: body 705 | description: Model instance data 706 | required: false 707 | schema: 708 | $ref: '#/definitions/AccountSummary' 709 | responses: 710 | '200': 711 | description: Request was successful 712 | schema: 713 | $ref: '#/definitions/AccountSummary' 714 | deprecated: false 715 | /AccountSummaries/findOne: 716 | get: 717 | tags: 718 | - AccountSummary 719 | summary: Find first instance of the model matched by filter from the data source. 720 | operationId: AccountSummary.findOne 721 | parameters: 722 | - name: filter 723 | in: query 724 | description: >- 725 | Filter defining fields, where, include, order, offset, and limit - 726 | must be a JSON-encoded string ({"something":"value"}) 727 | required: false 728 | type: string 729 | format: JSON 730 | responses: 731 | '200': 732 | description: Request was successful 733 | schema: 734 | $ref: '#/definitions/AccountSummary' 735 | deprecated: false 736 | /AccountSummaries/update: 737 | post: 738 | tags: 739 | - AccountSummary 740 | summary: 'Update instances of the model matched by {{where}} from the data source.' 741 | operationId: AccountSummary.updateAll 742 | parameters: 743 | - name: where 744 | in: query 745 | description: Criteria to match model instances 746 | required: false 747 | type: string 748 | format: JSON 749 | - name: data 750 | in: body 751 | description: An object of model property name/value pairs 752 | required: false 753 | schema: 754 | $ref: '#/definitions/AccountSummary' 755 | responses: 756 | '200': 757 | description: Request was successful 758 | schema: 759 | description: Information related to the outcome of the operation 760 | type: object 761 | deprecated: false 762 | /AccountSummaries/count: 763 | get: 764 | tags: 765 | - AccountSummary 766 | summary: Count instances of the model matched by where from the data source. 767 | operationId: AccountSummary.count 768 | parameters: 769 | - name: where 770 | in: query 771 | description: Criteria to match model instances 772 | required: false 773 | type: string 774 | format: JSON 775 | responses: 776 | '200': 777 | description: Request was successful 778 | schema: 779 | type: object 780 | properties: 781 | count: 782 | type: number 783 | format: double 784 | deprecated: false 785 | /AccountSummaries/change-stream: 786 | post: 787 | tags: 788 | - AccountSummary 789 | summary: Create a change stream. 790 | operationId: AccountSummary.createChangeStream__post_AccountSummaries_change-stream 791 | parameters: 792 | - name: options 793 | in: formData 794 | required: false 795 | type: string 796 | format: JSON 797 | responses: 798 | '200': 799 | description: Request was successful 800 | schema: 801 | type: file 802 | deprecated: false 803 | get: 804 | tags: 805 | - AccountSummary 806 | summary: Create a change stream. 807 | operationId: AccountSummary.createChangeStream__get_AccountSummaries_change-stream 808 | parameters: 809 | - name: options 810 | in: query 811 | required: false 812 | type: string 813 | format: JSON 814 | responses: 815 | '200': 816 | description: Request was successful 817 | schema: 818 | type: file 819 | deprecated: false 820 | definitions: 821 | Account: 822 | properties: 823 | customerNumber: 824 | type: string 825 | id: 826 | type: number 827 | format: double 828 | required: 829 | - customerNumber 830 | additionalProperties: false 831 | x-any: 832 | properties: {} 833 | AccountSummary: 834 | properties: 835 | accountNumber: 836 | type: string 837 | customerNumber: 838 | type: string 839 | balance: 840 | type: number 841 | format: double 842 | branch: 843 | type: string 844 | type: 845 | type: string 846 | avgBalance: 847 | type: number 848 | format: double 849 | minimumBalance: 850 | type: number 851 | format: double 852 | id: 853 | type: number 854 | format: double 855 | required: 856 | - accountNumber 857 | additionalProperties: false 858 | tags: 859 | - name: Account 860 | - name: AccountSummary 861 | -------------------------------------------------------------------------------- /services/customer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/customer-service 5 | WORKDIR /usr/src/customer-service 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/customer-service 9 | RUN npm install 10 | 11 | # Bundle app source 12 | COPY . /usr/src/customer-service 13 | 14 | EXPOSE 3000 15 | 16 | HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://localhost/vitals/docker || exit 1 17 | 18 | CMD [ "node", "." ] 19 | -------------------------------------------------------------------------------- /services/customer/common/models/address.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | module.exports = function(Address) { 5 | 6 | } -------------------------------------------------------------------------------- /services/customer/common/models/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Address", 3 | "idInjection": false, 4 | "options": { 5 | "validateUpsert": true 6 | }, 7 | "properties": { 8 | "customerId": { 9 | "type": "string", 10 | "id": true 11 | }, 12 | "type": { 13 | "type": "string" 14 | }, 15 | "street": { 16 | "type": "string" 17 | }, 18 | "state": { 19 | "type": "string" 20 | }, 21 | "city": { 22 | "type": "string" 23 | }, 24 | "zip": { 25 | "type": "string" 26 | }, 27 | "lastUpdated": { 28 | "type": "date" 29 | } 30 | }, 31 | "validations": [], 32 | "acls": [] 33 | } 34 | -------------------------------------------------------------------------------- /services/customer/common/models/customer.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | module.exports = function(Customer) { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /services/customer/common/models/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Customer", 3 | "idInjection": false, 4 | "options": { 5 | "validateUpsert": true 6 | }, 7 | "properties": { 8 | "customerNumber": { 9 | "type": "string", 10 | "id": true, 11 | "required": true 12 | }, 13 | "firstName": { 14 | "type": "string" 15 | }, 16 | "lastName": { 17 | "type": "string" 18 | }, 19 | "ssn": { 20 | "type": "string" 21 | }, 22 | "customerSince": { 23 | "type": "date" 24 | } 25 | }, 26 | "validations": [], 27 | "relations": { 28 | "address": { 29 | "type": "embedsMany", 30 | "model": "Address", 31 | "scope": { 32 | "include": "linked" 33 | } 34 | } 35 | }, 36 | "acls": [] 37 | } 38 | -------------------------------------------------------------------------------- /services/customer/common/models/vital.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const app = require('../../server/server'); 5 | const Promise = require('bluebird'); 6 | 7 | module.exports = function(Vital) { 8 | Vital.check = async function() { 9 | const status = { 10 | status: 'healthy', 11 | dependencies: {}, 12 | } 13 | const dsList = app.dataSources; 14 | const dsNames = Object.keys(dsList); 15 | dsNames.forEach(function(dsName) { 16 | status.dependencies[dsName] = checkVital(dsList[dsName]); 17 | }); 18 | status.dependencies = await Promise.props(status.dependencies); 19 | return status; 20 | }; 21 | function checkVital(ds) { 22 | return new Promise(function(resolve, reject) { 23 | ds.ping(function(err, result) { 24 | if (err) return reject({status: 'unhealthy'}); 25 | resolve({status: 'healthy'}); 26 | }); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /services/customer/common/models/vital.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vital", 3 | "idInjection": false, 4 | "base": "Model", 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": { 13 | "check": { 14 | "returns": [ 15 | { 16 | "arg": "vitals", 17 | "type": "Object", 18 | "root": true, 19 | "description": "health check" 20 | } 21 | ], 22 | "description": "do health check", 23 | "http": [ 24 | { 25 | "path": "/", 26 | "verb": "get" 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/customer/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "Customer": 3499224, 4 | "Address": 2 5 | }, 6 | "models": { 7 | "Customer": { 8 | "000343223": "{\"customerNumber\":\"000343223\",\"firstName\":\"Homer\",\"lastName\":\"Simpson\",\"ssn\":\"141-XX-X800\",\"customerSince\":\"2017-03-14T23:05:18.779Z\",\"addresses\":[{\"customerId\":\"000343223\",\"type\":\"Personal\",\"street\":\"742 Evergreen Terrace\",\"state\":\"OR\",\"city\":\"Springfield\",\"zip\":\"95555\",\"lastUpdated\":\"2017-03-14T23:05:18.599Z\"}]}", 9 | "003499223": "{\"customerNumber\":\"003499223\",\"firstName\":\"Akhil\",\"lastName\":\"Freerich\",\"ssn\":\"342-XX-XX24\",\"customerSince\":\"2017-03-14T23:26:45.225Z\",\"addresses\":[{\"customerId\":\"003499223\",\"type\":\"Personal\",\"street\":\"3005 chapel ave\",\"state\":\"NewJersey\",\"city\":\"Chapel Ave\",\"zip\":\"08002\",\"lastUpdated\":\"2017-03-14T23:26:45.225Z\"}]}" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/customer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customer", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "start": "node .", 8 | "posttest": "npm run lint && nsp check" 9 | }, 10 | "dependencies": { 11 | "compression": "^1.0.3", 12 | "cors": "^2.5.2", 13 | "helmet": "^1.3.0", 14 | "loopback": "^3.0.0", 15 | "loopback-boot": "^2.6.5", 16 | "loopback-component-explorer": "^4.0.0", 17 | "morgan": "^1.7.0", 18 | "serve-favicon": "^2.0.1", 19 | "strong-error-handler": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^2.13.1", 23 | "eslint-config-loopback": "^4.0.0", 24 | "nsp": "^2.1.0" 25 | }, 26 | "repository": { 27 | "type": "", 28 | "url": "" 29 | }, 30 | "license": "UNLICENSED", 31 | "description": "customer" 32 | } 33 | -------------------------------------------------------------------------------- /services/customer/server/boot/root.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(server) { 7 | // Install a `/` route that returns server status 8 | var router = server.loopback.Router(); 9 | router.get('/', server.loopback.status()); 10 | server.use(router); 11 | }; 12 | -------------------------------------------------------------------------------- /services/customer/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/customer/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 80, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "handleErrors": false, 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/customer/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "CustomerDB": { 3 | "name": "db", 4 | "connector": "memory", 5 | "file": "data.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /services/customer/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/customer/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {}, 4 | "morgan": { 5 | "params": ["dev"] 6 | } 7 | }, 8 | "initial": { 9 | "compression": {}, 10 | "cors": { 11 | "params": { 12 | "origin": true, 13 | "credentials": true, 14 | "maxAge": 86400 15 | } 16 | }, 17 | "helmet#xssFilter": {}, 18 | "helmet#frameguard": { 19 | "params": [ 20 | "deny" 21 | ] 22 | }, 23 | "helmet#hsts": { 24 | "params": { 25 | "maxAge": 0, 26 | "includeSubdomains": true 27 | } 28 | }, 29 | "helmet#hidePoweredBy": {}, 30 | "helmet#ieNoOpen": {}, 31 | "helmet#noSniff": {}, 32 | "helmet#noCache": { 33 | "enabled": false 34 | } 35 | }, 36 | "session": {}, 37 | "auth": {}, 38 | "parse": {}, 39 | "routes": { 40 | "loopback#rest": { 41 | "paths": [ 42 | "${restApiRoot}" 43 | ] 44 | } 45 | }, 46 | "files": {}, 47 | "final": { 48 | "loopback#urlNotFound": {} 49 | }, 50 | "final:after": { 51 | "strong-error-handler": {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /services/customer/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "Customer": { 17 | "dataSource": "CustomerDB", 18 | "public": true 19 | }, 20 | "Address": { 21 | "dataSource": "CustomerDB", 22 | "public": true 23 | }, 24 | "Vital": { 25 | "dataSource": null, 26 | "public": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/customer/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | var loopback = require('loopback'); 7 | var boot = require('loopback-boot'); 8 | 9 | var app = module.exports = loopback(); 10 | 11 | app.use((req, res, next) => { 12 | setTimeout(next, 3000); 13 | }); 14 | 15 | app.get("/vitals/docker", (req, res) => { 16 | res.send("ok"); 17 | }); 18 | 19 | app.start = function() { 20 | // start the web server 21 | return app.listen(function() { 22 | app.emit('started'); 23 | var baseUrl = app.get('url').replace(/\/$/, ''); 24 | console.log('Web server listening at: %s', baseUrl); 25 | if (app.get('loopback-component-explorer')) { 26 | var explorerPath = app.get('loopback-component-explorer').mountPath; 27 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 28 | } 29 | }); 30 | }; 31 | 32 | // Bootstrap the application, configure models, datasources and middleware. 33 | // Sub-apps like REST API are mounted via boot scripts. 34 | boot(app, __dirname, function(err) { 35 | if (err) throw err; 36 | 37 | // start the server if `$ node server.js` 38 | if (require.main === module) 39 | app.start(); 40 | }); 41 | -------------------------------------------------------------------------------- /services/customer/test.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: 1.0.0 4 | title: customer 5 | basePath: /api 6 | consumes: 7 | - application/json 8 | - application/x-www-form-urlencoded 9 | - application/xml 10 | - text/xml 11 | produces: 12 | - application/json 13 | - application/xml 14 | - text/xml 15 | - application/javascript 16 | - text/javascript 17 | paths: 18 | /Customers: 19 | post: 20 | tags: 21 | - Customer 22 | summary: Create a new instance of the model and persist it into the data source. 23 | operationId: Customer.create 24 | parameters: 25 | - name: data 26 | in: body 27 | description: Model instance data 28 | required: false 29 | schema: 30 | $ref: '#/definitions/Customer' 31 | responses: 32 | '200': 33 | description: Request was successful 34 | schema: 35 | $ref: '#/definitions/Customer' 36 | deprecated: false 37 | patch: 38 | tags: 39 | - Customer 40 | summary: >- 41 | Patch an existing model instance or insert a new one into the data 42 | source. 43 | operationId: Customer.patchOrCreate 44 | parameters: 45 | - name: data 46 | in: body 47 | description: Model instance data 48 | required: false 49 | schema: 50 | $ref: '#/definitions/Customer' 51 | responses: 52 | '200': 53 | description: Request was successful 54 | schema: 55 | $ref: '#/definitions/Customer' 56 | deprecated: false 57 | put: 58 | tags: 59 | - Customer 60 | summary: >- 61 | Replace an existing model instance or insert a new one into the data 62 | source. 63 | operationId: Customer.replaceOrCreate__put_Customers 64 | parameters: 65 | - name: data 66 | in: body 67 | description: Model instance data 68 | required: false 69 | schema: 70 | $ref: '#/definitions/Customer' 71 | responses: 72 | '200': 73 | description: Request was successful 74 | schema: 75 | $ref: '#/definitions/Customer' 76 | deprecated: false 77 | get: 78 | tags: 79 | - Customer 80 | summary: Find all instances of the model matched by filter from the data source. 81 | operationId: Customer.find 82 | parameters: 83 | - name: filter 84 | in: query 85 | description: >- 86 | Filter defining fields, where, include, order, offset, and limit - 87 | must be a JSON-encoded string ({"something":"value"}) 88 | required: false 89 | type: string 90 | format: JSON 91 | responses: 92 | '200': 93 | description: Request was successful 94 | schema: 95 | type: array 96 | items: 97 | $ref: '#/definitions/Customer' 98 | deprecated: false 99 | /Customers/replaceOrCreate: 100 | post: 101 | tags: 102 | - Customer 103 | summary: >- 104 | Replace an existing model instance or insert a new one into the data 105 | source. 106 | operationId: Customer.replaceOrCreate__post_Customers_replaceOrCreate 107 | parameters: 108 | - name: data 109 | in: body 110 | description: Model instance data 111 | required: false 112 | schema: 113 | $ref: '#/definitions/Customer' 114 | responses: 115 | '200': 116 | description: Request was successful 117 | schema: 118 | $ref: '#/definitions/Customer' 119 | deprecated: false 120 | /Customers/upsertWithWhere: 121 | post: 122 | tags: 123 | - Customer 124 | summary: >- 125 | Update an existing model instance or insert a new one into the data 126 | source based on the where criteria. 127 | operationId: Customer.upsertWithWhere 128 | parameters: 129 | - name: where 130 | in: query 131 | description: Criteria to match model instances 132 | required: false 133 | type: string 134 | format: JSON 135 | - name: data 136 | in: body 137 | description: An object of model property name/value pairs 138 | required: false 139 | schema: 140 | $ref: '#/definitions/Customer' 141 | responses: 142 | '200': 143 | description: Request was successful 144 | schema: 145 | $ref: '#/definitions/Customer' 146 | deprecated: false 147 | '/Customers/{id}/exists': 148 | get: 149 | tags: 150 | - Customer 151 | summary: Check whether a model instance exists in the data source. 152 | operationId: 'Customer.exists__get_Customers_{id}_exists' 153 | parameters: 154 | - name: id 155 | in: path 156 | description: Model id 157 | required: true 158 | type: string 159 | format: JSON 160 | responses: 161 | '200': 162 | description: Request was successful 163 | schema: 164 | type: object 165 | properties: 166 | exists: 167 | type: boolean 168 | deprecated: false 169 | '/Customers/{id}': 170 | head: 171 | tags: 172 | - Customer 173 | summary: Check whether a model instance exists in the data source. 174 | operationId: 'Customer.exists__head_Customers_{id}' 175 | parameters: 176 | - name: id 177 | in: path 178 | description: Model id 179 | required: true 180 | type: string 181 | format: JSON 182 | responses: 183 | '200': 184 | description: Request was successful 185 | schema: 186 | type: object 187 | properties: 188 | exists: 189 | type: boolean 190 | deprecated: false 191 | get: 192 | tags: 193 | - Customer 194 | summary: 'Find a model instance by {{id}} from the data source.' 195 | operationId: Customer.findById 196 | parameters: 197 | - name: id 198 | in: path 199 | description: Model id 200 | required: true 201 | type: string 202 | format: JSON 203 | - name: filter 204 | in: query 205 | description: >- 206 | Filter defining fields and include - must be a JSON-encoded string 207 | ({"something":"value"}) 208 | required: false 209 | type: string 210 | format: JSON 211 | responses: 212 | '200': 213 | description: Request was successful 214 | schema: 215 | $ref: '#/definitions/Customer' 216 | deprecated: false 217 | put: 218 | tags: 219 | - Customer 220 | summary: >- 221 | Replace attributes for a model instance and persist it into the data 222 | source. 223 | operationId: 'Customer.replaceById__put_Customers_{id}' 224 | parameters: 225 | - name: id 226 | in: path 227 | description: Model id 228 | required: true 229 | type: string 230 | format: JSON 231 | - name: data 232 | in: body 233 | description: Model instance data 234 | required: false 235 | schema: 236 | $ref: '#/definitions/Customer' 237 | responses: 238 | '200': 239 | description: Request was successful 240 | schema: 241 | $ref: '#/definitions/Customer' 242 | deprecated: false 243 | delete: 244 | tags: 245 | - Customer 246 | summary: 'Delete a model instance by {{id}} from the data source.' 247 | operationId: Customer.deleteById 248 | parameters: 249 | - name: id 250 | in: path 251 | description: Model id 252 | required: true 253 | type: string 254 | format: JSON 255 | responses: 256 | '200': 257 | description: Request was successful 258 | schema: 259 | type: object 260 | deprecated: false 261 | patch: 262 | tags: 263 | - Customer 264 | summary: >- 265 | Patch attributes for a model instance and persist it into the data 266 | source. 267 | operationId: Customer.prototype.patchAttributes 268 | parameters: 269 | - name: data 270 | in: body 271 | description: An object of model property name/value pairs 272 | required: false 273 | schema: 274 | $ref: '#/definitions/Customer' 275 | - name: id 276 | in: path 277 | description: Customer id 278 | required: true 279 | type: string 280 | format: JSON 281 | responses: 282 | '200': 283 | description: Request was successful 284 | schema: 285 | $ref: '#/definitions/Customer' 286 | deprecated: false 287 | '/Customers/{id}/replace': 288 | post: 289 | tags: 290 | - Customer 291 | summary: >- 292 | Replace attributes for a model instance and persist it into the data 293 | source. 294 | operationId: 'Customer.replaceById__post_Customers_{id}_replace' 295 | parameters: 296 | - name: id 297 | in: path 298 | description: Model id 299 | required: true 300 | type: string 301 | format: JSON 302 | - name: data 303 | in: body 304 | description: Model instance data 305 | required: false 306 | schema: 307 | $ref: '#/definitions/Customer' 308 | responses: 309 | '200': 310 | description: Request was successful 311 | schema: 312 | $ref: '#/definitions/Customer' 313 | deprecated: false 314 | /Customers/findOne: 315 | get: 316 | tags: 317 | - Customer 318 | summary: Find first instance of the model matched by filter from the data source. 319 | operationId: Customer.findOne 320 | parameters: 321 | - name: filter 322 | in: query 323 | description: >- 324 | Filter defining fields, where, include, order, offset, and limit - 325 | must be a JSON-encoded string ({"something":"value"}) 326 | required: false 327 | type: string 328 | format: JSON 329 | responses: 330 | '200': 331 | description: Request was successful 332 | schema: 333 | $ref: '#/definitions/Customer' 334 | deprecated: false 335 | /Customers/update: 336 | post: 337 | tags: 338 | - Customer 339 | summary: 'Update instances of the model matched by {{where}} from the data source.' 340 | operationId: Customer.updateAll 341 | parameters: 342 | - name: where 343 | in: query 344 | description: Criteria to match model instances 345 | required: false 346 | type: string 347 | format: JSON 348 | - name: data 349 | in: body 350 | description: An object of model property name/value pairs 351 | required: false 352 | schema: 353 | $ref: '#/definitions/Customer' 354 | responses: 355 | '200': 356 | description: Request was successful 357 | schema: 358 | description: Information related to the outcome of the operation 359 | type: object 360 | deprecated: false 361 | /Customers/count: 362 | get: 363 | tags: 364 | - Customer 365 | summary: Count instances of the model matched by where from the data source. 366 | operationId: Customer.count 367 | parameters: 368 | - name: where 369 | in: query 370 | description: Criteria to match model instances 371 | required: false 372 | type: string 373 | format: JSON 374 | responses: 375 | '200': 376 | description: Request was successful 377 | schema: 378 | type: object 379 | properties: 380 | count: 381 | type: number 382 | format: double 383 | deprecated: false 384 | /Customers/change-stream: 385 | post: 386 | tags: 387 | - Customer 388 | summary: Create a change stream. 389 | operationId: Customer.createChangeStream__post_Customers_change-stream 390 | parameters: 391 | - name: options 392 | in: formData 393 | required: false 394 | type: string 395 | format: JSON 396 | responses: 397 | '200': 398 | description: Request was successful 399 | schema: 400 | type: file 401 | deprecated: false 402 | get: 403 | tags: 404 | - Customer 405 | summary: Create a change stream. 406 | operationId: Customer.createChangeStream__get_Customers_change-stream 407 | parameters: 408 | - name: options 409 | in: query 410 | required: false 411 | type: string 412 | format: JSON 413 | responses: 414 | '200': 415 | description: Request was successful 416 | schema: 417 | type: file 418 | deprecated: false 419 | definitions: 420 | Customer: 421 | properties: 422 | customerNumber: 423 | type: string 424 | firstName: 425 | type: string 426 | lastName: 427 | type: string 428 | ssn: 429 | type: string 430 | customerSince: 431 | type: string 432 | id: 433 | type: number 434 | format: double 435 | additionalProperties: false 436 | x-any: 437 | properties: {} 438 | tags: 439 | - name: Customer 440 | -------------------------------------------------------------------------------- /services/facade/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /services/facade/.eslintignore: -------------------------------------------------------------------------------- 1 | /client/ -------------------------------------------------------------------------------- /services/facade/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback" 3 | } -------------------------------------------------------------------------------- /services/facade/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-loopback": {} 3 | } -------------------------------------------------------------------------------- /services/facade/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/facade 5 | WORKDIR /usr/src/facade 6 | 7 | ENV DEBUG loopback:connector:swagger 8 | 9 | # Install app dependencies 10 | COPY package.json /usr/src/facade 11 | RUN npm install 12 | 13 | # Bundle app source 14 | COPY . /usr/src/facade 15 | 16 | EXPOSE 3000 17 | 18 | HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://localhost/vitals/docker || exit 1 19 | 20 | CMD ["node", "."] 21 | -------------------------------------------------------------------------------- /services/facade/common/models/account-cache.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(AccountCache) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /services/facade/common/models/account-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AccountCache", 3 | "base": "KeyValueModel", 4 | "properties": {}, 5 | "validations": [], 6 | "relations": {}, 7 | "acls": [], 8 | "methods": {} 9 | } 10 | -------------------------------------------------------------------------------- /services/facade/common/models/account.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | const app = require('../../server/server'); 7 | const Promise = require('bluebird'); 8 | 9 | // NOTE: View `console.log` output in the docker-compose logs 10 | 11 | module.exports = function(Account) { 12 | Account.getAccount = async function(accountNumber) { 13 | return Account.Account_findById({id: accountNumber}).get('obj'); 14 | }; 15 | 16 | Account.getAccountSummary = async function(accountNumber, cache) { 17 | console.log('checking shared cache'); 18 | const Cache = app.models.Cache; 19 | const key = '/api/Account/summary?accountNumber=' + accountNumber; 20 | let accountSummary = await Cache.get(key); 21 | if (accountSummary && cache !== false) { 22 | const ttl = await Cache.ttl(key); 23 | console.log('cache hit, return cache data, ttl:', ttl); 24 | accountSummary.cache = ttl; 25 | return accountSummary; 26 | } 27 | 28 | console.log('cache miss, retrieve data from microservices'); 29 | const {Transaction} = app.models; 30 | accountSummary = await Promise.props({ 31 | account: Account.getAccount(accountNumber), 32 | transactions: app.models.Transaction.find(accountNumber), 33 | }) 34 | .then(data => { 35 | const {Customer} = app.models; 36 | const customerNumber = data.account.customerNumber; 37 | data.customer = Customer.find(customerNumber); 38 | return Promise.props(data); 39 | }); 40 | 41 | console.log('update shared cache with returned data'); 42 | await Cache.set(key, accountSummary, {ttl: 60000}); 43 | 44 | return accountSummary; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /services/facade/common/models/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Account", 3 | "base": "Model", 4 | "idInjection": false, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "accountNumber": { 10 | "type": "string", 11 | "id": true, 12 | "required": true 13 | }, 14 | "customerNumber": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "balance": { 19 | "type": "number", 20 | "required": true 21 | }, 22 | "transactions": { 23 | "type": [ 24 | "object" 25 | ], 26 | "required": true 27 | } 28 | }, 29 | "validations": [], 30 | "relations": {}, 31 | "acls": [], 32 | "methods": { 33 | "getAccount": { 34 | "description": "Get the account for the given account number.", 35 | "http": { 36 | "path": "/:accountNumber", 37 | "verb": "get" 38 | }, 39 | "accepts": { 40 | "arg": "accountNumber", 41 | "type": "string", 42 | "required": true, 43 | "description": "The account number. (Eg. CHK52321122)", 44 | "http": {"source": "path"} 45 | }, 46 | "returns": { 47 | "arg": "account", 48 | "type": "Account", 49 | "root": true, 50 | "description": "The account" 51 | } 52 | }, 53 | "getAccountSummary": { 54 | "description": "Get a summary of the recent transactions and other account/customer information.", 55 | "http": { 56 | "path": "/summary", 57 | "verb": "get" 58 | }, 59 | "accepts": [ 60 | { 61 | "arg": "accountNumber", 62 | "type": "string", 63 | "required": true, 64 | "description": "The account number for the summary", 65 | "http": {"source": "query"} 66 | }, 67 | { 68 | "arg": "cache", 69 | "type": "boolean", 70 | "description": "Enable the cache. Cache enabled by default.", 71 | "http": {"source": "query"} 72 | } 73 | ], 74 | "returns": { 75 | "arg": "summary", 76 | "type": "object", 77 | "root": true, 78 | "description": "The account summary" 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /services/facade/common/models/cache.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(Cache) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /services/facade/common/models/cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cache", 3 | "base": "KeyValueModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": {} 13 | } 14 | -------------------------------------------------------------------------------- /services/facade/common/models/customer-cache.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(CustomerCache) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /services/facade/common/models/customer-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CustomerCache", 3 | "base": "KeyValueModel", 4 | "properties": {}, 5 | "validations": [], 6 | "relations": {}, 7 | "acls": [], 8 | "methods": {} 9 | } 10 | -------------------------------------------------------------------------------- /services/facade/common/models/customer.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(Customer) { 7 | Customer.find = function(customerNumber) { 8 | // clean this up with proper promise return shortcut 9 | return Customer.Customer_findById({id: customerNumber}).then(res => { 10 | return res.obj; 11 | }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /services/facade/common/models/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Customer", 3 | "base": "Model", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": {} 13 | } 14 | -------------------------------------------------------------------------------- /services/facade/common/models/transaction-cache.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(TransactionCache) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /services/facade/common/models/transaction-cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TransactionCache", 3 | "base": "KeyValueModel", 4 | "properties": {}, 5 | "validations": [], 6 | "relations": {}, 7 | "acls": [], 8 | "methods": {} 9 | } 10 | -------------------------------------------------------------------------------- /services/facade/common/models/transaction.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(Transaction) { 7 | Transaction.find = function(accountNumber) { 8 | // clean this up with proper promise return shortcut 9 | return Transaction.Transaction_queryByAccount({accountNumber: accountNumber}).then(res => { 10 | return res.obj; 11 | }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /services/facade/common/models/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Transaction", 3 | "base": "Model", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": {} 13 | } 14 | -------------------------------------------------------------------------------- /services/facade/common/models/vital.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const app = require('../../server/server'); 5 | const Promise = require('bluebird'); 6 | 7 | module.exports = function(Vital) { 8 | Vital.check = async function() { 9 | const status = { 10 | status: 'healthy', 11 | dependencies: {}, 12 | } 13 | const modelList = app.models; 14 | const modelNames = Object.keys(modelList); 15 | modelNames.forEach(function(modelName) { 16 | status.dependencies[modelName] = checkVital(modelList[modelName]); 17 | }); 18 | status.dependencies = await Promise.props(status.dependencies); 19 | return status; 20 | }; 21 | function checkVital(model) { 22 | if(model.Vital_check) { 23 | return new Promise(function(resolve, reject) { 24 | var startTime = new Date(); 25 | model.Vital_check(function(result) { 26 | var totalTime = new Date() - startTime; 27 | if (result.status !== 200) return resolve({status: 'unhealthy'}); 28 | resolve(Object.assign(result.obj, {latency: totalTime/1000})); 29 | }); 30 | }); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /services/facade/common/models/vital.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vital", 3 | "idInjection": false, 4 | "base": "Model", 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": { 13 | "check": { 14 | "returns": [ 15 | { 16 | "arg": "vitals", 17 | "type": "Object", 18 | "root": true, 19 | "description": "health check" 20 | } 21 | ], 22 | "description": "do health check", 23 | "http": [ 24 | { 25 | "path": "/", 26 | "verb": "get" 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/facade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facade", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "start": "node .", 8 | "test": "./bin/get-account-summary" 9 | }, 10 | "dependencies": { 11 | "bluebird": "latest", 12 | "compression": "^1.0.3", 13 | "cors": "^2.5.2", 14 | "helmet": "^1.3.0", 15 | "loopback": "^3.0.0", 16 | "loopback-boot": "^2.6.5", 17 | "loopback-component-explorer": "^4.0.0", 18 | "loopback-connector-swagger": "^3.1.0", 19 | "morgan": "^1.7.0", 20 | "serve-favicon": "^2.0.1", 21 | "strong-error-handler": "^1.0.1" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^3.17.1", 25 | "eslint-config-loopback": "^8.0.0", 26 | "nsp": "^2.1.0" 27 | }, 28 | "repository": { 29 | "type": "", 30 | "url": "" 31 | }, 32 | "license": "UNLICENSED", 33 | "description": "facade" 34 | } 35 | -------------------------------------------------------------------------------- /services/facade/server/boot/root.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(server) { 7 | // Install a `/` route that returns server status 8 | var router = server.loopback.Router(); 9 | router.get('/', server.loopback.status()); 10 | server.use(router); 11 | }; 12 | -------------------------------------------------------------------------------- /services/facade/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/facade/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 80, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "handleErrors": false, 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/facade/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": { 3 | "name": "account", 4 | "connector": "swagger", 5 | "spec": "http://account/explorer/swagger.json", 6 | "cache": { 7 | "model": "AccountCache", 8 | "ttl": 60000 9 | } 10 | }, 11 | "customer": { 12 | "name": "customer", 13 | "connector": "swagger", 14 | "spec": "http://customer/explorer/swagger.json", 15 | "cache": { 16 | "model": "CustomerCache", 17 | "ttl": 60000 18 | } 19 | }, 20 | "transaction": { 21 | "name": "transaction", 22 | "connector": "swagger", 23 | "spec": "http://transaction/explorer/swagger.json", 24 | "cache": { 25 | "model": "TransactionCache", 26 | "ttl": 60000 27 | } 28 | }, 29 | "shared-cache": { 30 | "name": "shared-cache", 31 | "connector": "kv-memory" 32 | }, 33 | "private-cache": { 34 | "name": "private-cache", 35 | "connector": "kv-memory" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /services/facade/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/facade/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {}, 4 | "morgan": { 5 | "params": ["dev"] 6 | } 7 | }, 8 | "initial": { 9 | "compression": {}, 10 | "cors": { 11 | "params": { 12 | "origin": true, 13 | "credentials": true, 14 | "maxAge": 86400 15 | } 16 | }, 17 | "helmet#xssFilter": {}, 18 | "helmet#frameguard": { 19 | "params": [ 20 | "deny" 21 | ] 22 | }, 23 | "helmet#hsts": { 24 | "params": { 25 | "maxAge": 0, 26 | "includeSubdomains": true 27 | } 28 | }, 29 | "helmet#hidePoweredBy": {}, 30 | "helmet#ieNoOpen": {}, 31 | "helmet#noSniff": {}, 32 | "helmet#noCache": { 33 | "enabled": false 34 | } 35 | }, 36 | "session": {}, 37 | "auth": {}, 38 | "parse": {}, 39 | "routes": { 40 | "loopback#rest": { 41 | "paths": [ 42 | "${restApiRoot}" 43 | ] 44 | } 45 | }, 46 | "files": {}, 47 | "final": { 48 | "loopback#urlNotFound": {} 49 | }, 50 | "final:after": { 51 | "strong-error-handler": {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /services/facade/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "Account": { 17 | "dataSource": "account", 18 | "public": true 19 | }, 20 | "Cache": { 21 | "dataSource": "shared-cache", 22 | "public": false 23 | }, 24 | "AccountCache": { 25 | "dataSource": "private-cache", 26 | "public": false 27 | }, 28 | "Customer": { 29 | "dataSource": "customer", 30 | "public": false 31 | }, 32 | "CustomerCache": { 33 | "dataSource": "private-cache", 34 | "public": false 35 | }, 36 | "Transaction": { 37 | "dataSource": "transaction", 38 | "public": false 39 | }, 40 | "TransactionCache": { 41 | "dataSource": "private-cache", 42 | "public": false 43 | }, 44 | "Vital": { 45 | "dataSource": null, 46 | "public": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /services/facade/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | var loopback = require('loopback'); 7 | var boot = require('loopback-boot'); 8 | var app = module.exports = loopback(); 9 | 10 | app.get('/vitals/docker', (req, res) => { 11 | res.send('ok'); 12 | }); 13 | 14 | app.start = function() { 15 | // start the web server 16 | return app.listen(function() { 17 | app.emit('started'); 18 | var baseUrl = app.get('url').replace(/\/$/, ''); 19 | console.log('Web server listening at: %s', baseUrl); 20 | if (app.get('loopback-component-explorer')) { 21 | var explorerPath = app.get('loopback-component-explorer').mountPath; 22 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 23 | } 24 | }); 25 | }; 26 | 27 | // Bootstrap the application, configure models, datasources and middleware. 28 | // Sub-apps like REST API are mounted via boot scripts. 29 | boot(app, __dirname, function(err) { 30 | if (err) throw err; 31 | 32 | // start the server if `$ node server.js` 33 | if (require.main === module) 34 | app.start(); 35 | }); 36 | -------------------------------------------------------------------------------- /services/transaction/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/transaction-service 5 | WORKDIR /usr/src/transaction-service 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/transaction-service 9 | RUN npm install 10 | 11 | # Bundle app source 12 | COPY . /usr/src/transaction-service 13 | 14 | EXPOSE 3000 15 | 16 | HEALTHCHECK --interval=1m --timeout=3s CMD curl --fail http://localhost/vitals/docker || exit 1 17 | 18 | CMD ["node", "."] 19 | -------------------------------------------------------------------------------- /services/transaction/common/models/cheque.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(Cheque) { 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /services/transaction/common/models/cheque.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cheque", 3 | "base": "PersistedModel", 4 | "idInjection": true, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": { 9 | "number": { 10 | "type": "number", 11 | "required": true 12 | }, 13 | "amount": { 14 | "type": "number", 15 | "required": true 16 | } 17 | }, 18 | "validations": [], 19 | "relations": {}, 20 | "acls": [], 21 | "methods": {} 22 | } 23 | -------------------------------------------------------------------------------- /services/transaction/common/models/transaction.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | module.exports = function(Transaction) { 5 | Transaction.validatesInclusionOf('transactionType', {in: ['debit', 'credit']}); 6 | 7 | Transaction.queryByAccount = function(accountNumber) { 8 | return Transaction.find({ 9 | where: { 10 | accountNo: accountNumber 11 | } 12 | }); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /services/transaction/common/models/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Transaction", 3 | "idInjection": false, 4 | "options": { 5 | "validateUpsert": true 6 | }, 7 | "properties": { 8 | "TransactionId": { 9 | "type": "string", 10 | "id": true, 11 | "required": true 12 | }, 13 | "dateTime": { 14 | "type": "date", 15 | "required": true 16 | }, 17 | "accountNo": { 18 | "type": "string", 19 | "required": true 20 | }, 21 | "amount": { 22 | "type": "number", 23 | "required": true 24 | }, 25 | "transactionType": { 26 | "type": "string", 27 | "required": true 28 | } 29 | }, 30 | "validations": [], 31 | "relations": {}, 32 | "acls": [], 33 | "methods": { 34 | "queryByAccount": { 35 | "accepts": [ 36 | { 37 | "arg": "accountNumber", 38 | "type": "string", 39 | "required": true, 40 | "description": "account number", 41 | "http": { 42 | "source": "query" 43 | } 44 | } 45 | ], 46 | "returns": [ 47 | { 48 | "arg": "transactions", 49 | "type": "object", 50 | "root": true, 51 | "description": "The transaction list" 52 | } 53 | ], 54 | "description": "list of transactions on the account", 55 | "http": [ 56 | { 57 | "path": "/account", 58 | "verb": "get" 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /services/transaction/common/models/vital.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const app = require('../../server/server'); 5 | const Promise = require('bluebird'); 6 | 7 | module.exports = function(Vital) { 8 | Vital.check = async function() { 9 | const status = { 10 | status: 'healthy', 11 | dependencies: {}, 12 | } 13 | const dsList = app.dataSources; 14 | const dsNames = Object.keys(dsList); 15 | dsNames.forEach(function(dsName) { 16 | status.dependencies[dsName] = checkVital(dsList[dsName]); 17 | }); 18 | status.dependencies = await Promise.props(status.dependencies); 19 | return status; 20 | }; 21 | function checkVital(ds) { 22 | return new Promise(function(resolve, reject) { 23 | ds.ping(function(err, result) { 24 | if (err) return reject({status: 'unhealthy'}); 25 | resolve({status: 'healthy'}); 26 | }); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /services/transaction/common/models/vital.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vital", 3 | "base": "Model", 4 | "idInjection": false, 5 | "options": { 6 | "validateUpsert": true 7 | }, 8 | "properties": {}, 9 | "validations": [], 10 | "relations": {}, 11 | "acls": [], 12 | "methods": { 13 | "check": { 14 | "returns": [ 15 | { 16 | "arg": "vitals", 17 | "type": "Object", 18 | "root": true, 19 | "description": "health check" 20 | } 21 | ], 22 | "description": "do health check", 23 | "http": [ 24 | { 25 | "path": "/", 26 | "verb": "get" 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/transaction/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": { 3 | "Transaction": 13 4 | }, 5 | "models": { 6 | "Transaction": { 7 | "DEBIT0001": "{\"TransactionId\":\"DEBIT0001\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":20,\"transactionType\":\"debit\"}", 8 | "DEBIT0002": "{\"TransactionId\":\"DEBIT0002\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":20,\"transactionType\":\"debit\"}", 9 | "DEBIT0003": "{\"TransactionId\":\"DEBIT0003\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":40,\"transactionType\":\"credit\"}", 10 | "DEBIT0004": "{\"TransactionId\":\"DEBIT0004\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":50,\"transactionType\":\"credit\"}", 11 | "DEBIT0005": "{\"TransactionId\":\"DEBIT0005\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":50,\"transactionType\":\"credit\"}", 12 | "DEBIT0007": "{\"TransactionId\":\"DEBIT0007\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK54520000\",\"amount\":100,\"transactionType\":\"credit\"}", 13 | "DEBIT0009": "{\"TransactionId\":\"DEBIT0009\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321199\",\"amount\":140,\"transactionType\":\"credit\"}", 14 | "DEBIT0010": "{\"TransactionId\":\"DEBIT0010\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321199\",\"amount\":20,\"transactionType\":\"debit\"}", 15 | "DEBIT0011": "{\"TransactionId\":\"DEBIT0011\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":300,\"transactionType\":\"credit\"}", 16 | "DEBIT0012": "{\"TransactionId\":\"DEBIT0012\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":100,\"transactionType\":\"debit\"}", 17 | "DEBIT0013": "{\"TransactionId\":\"DEBIT0013\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":50,\"transactionType\":\"debit\"}" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /services/transaction/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transaction", 3 | "version": "1.0.0", 4 | "main": "server/server.js", 5 | "scripts": { 6 | "lint": "eslint .", 7 | "start": "node .", 8 | "posttest": "npm run lint && nsp check" 9 | }, 10 | "dependencies": { 11 | "compression": "^1.0.3", 12 | "cors": "^2.5.2", 13 | "helmet": "^1.3.0", 14 | "loopback": "^3.0.0", 15 | "loopback-boot": "^2.6.5", 16 | "loopback-component-explorer": "^4.0.0", 17 | "loopback-connector-mongodb": "^1.18.1", 18 | "morgan": "^1.7.0", 19 | "serve-favicon": "^2.0.1", 20 | "strong-error-handler": "^1.0.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^2.13.1", 24 | "eslint-config-loopback": "^4.0.0", 25 | "nsp": "^2.1.0" 26 | }, 27 | "repository": { 28 | "type": "", 29 | "url": "" 30 | }, 31 | "license": "UNLICENSED", 32 | "description": "transaction" 33 | } 34 | -------------------------------------------------------------------------------- /services/transaction/server/boot/root.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(server) { 7 | // Install a `/` route that returns server status 8 | var router = server.loopback.Router(); 9 | router.get('/', server.loopback.status()); 10 | server.use(router); 11 | }; 12 | -------------------------------------------------------------------------------- /services/transaction/server/component-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "loopback-component-explorer": { 3 | "mountPath": "/explorer" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/transaction/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "restApiRoot": "/api", 3 | "host": "0.0.0.0", 4 | "port": 80, 5 | "remoting": { 6 | "context": false, 7 | "rest": { 8 | "handleErrors": false, 9 | "normalizeHttpPath": false, 10 | "xml": false 11 | }, 12 | "json": { 13 | "strict": false, 14 | "limit": "100kb" 15 | }, 16 | "urlencoded": { 17 | "extended": true, 18 | "limit": "100kb" 19 | }, 20 | "cors": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/transaction/server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "TransactionDB": { 3 | "name": "TransactionDB", 4 | "connector": "memory", 5 | "file": "data.json" 6 | }, 7 | "mongoService": { 8 | "host": "mongo", 9 | "port": 27017, 10 | "connector": "mongodb" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /services/transaction/server/middleware.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "final:after": { 3 | "strong-error-handler": { 4 | "params": { 5 | "debug": true, 6 | "log": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/transaction/server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": {}, 4 | "morgan": { 5 | "params": [ 6 | "dev" 7 | ] 8 | } 9 | }, 10 | "initial": { 11 | "compression": {}, 12 | "cors": { 13 | "params": { 14 | "origin": true, 15 | "credentials": true, 16 | "maxAge": 86400 17 | } 18 | }, 19 | "helmet#xssFilter": {}, 20 | "helmet#frameguard": { 21 | "params": [ 22 | "deny" 23 | ] 24 | }, 25 | "helmet#hsts": { 26 | "params": { 27 | "maxAge": 0, 28 | "includeSubdomains": true 29 | } 30 | }, 31 | "helmet#hidePoweredBy": {}, 32 | "helmet#ieNoOpen": {}, 33 | "helmet#noSniff": {}, 34 | "helmet#noCache": { 35 | "enabled": false 36 | } 37 | }, 38 | "session": {}, 39 | "auth": {}, 40 | "parse": {}, 41 | "routes": { 42 | "loopback#rest": { 43 | "paths": [ 44 | "${restApiRoot}" 45 | ] 46 | } 47 | }, 48 | "files": {}, 49 | "final": { 50 | "loopback#urlNotFound": {} 51 | }, 52 | "final:after": { 53 | "strong-error-handler": {} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /services/transaction/server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "Transaction": { 17 | "dataSource": "TransactionDB", 18 | "public": true 19 | }, 20 | "Vital": { 21 | "dataSource": null, 22 | "public": true 23 | }, 24 | "Cheque": { 25 | "dataSource": "mongoService", 26 | "public": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/transaction/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | 'use strict'; 5 | 6 | var loopback = require('loopback'); 7 | var boot = require('loopback-boot'); 8 | 9 | var app = module.exports = loopback(); 10 | 11 | app.use((req, res, next) => { 12 | setTimeout(next, 3000); 13 | }); 14 | 15 | app.get("/vitals/docker", (req, res) => { 16 | res.send("ok"); 17 | }); 18 | 19 | app.start = function() { 20 | // start the web server 21 | return app.listen(function() { 22 | app.emit('started'); 23 | var baseUrl = app.get('url').replace(/\/$/, ''); 24 | console.log('Web server listening at: %s', baseUrl); 25 | if (app.get('loopback-component-explorer')) { 26 | var explorerPath = app.get('loopback-component-explorer').mountPath; 27 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 28 | } 29 | }); 30 | }; 31 | 32 | // Bootstrap the application, configure models, datasources and middleware. 33 | // Sub-apps like REST API are mounted via boot scripts. 34 | boot(app, __dirname, function(err) { 35 | if (err) throw err; 36 | 37 | // start the server if `$ node server.js` 38 | if (require.main === module) 39 | app.start(); 40 | }); 41 | -------------------------------------------------------------------------------- /spec/facade.yml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Facade 4 | version: v1 5 | produces: 6 | - application/json 7 | consumes: 8 | - application/json 9 | paths: 10 | /vitals: 11 | get: 12 | /account: 13 | get: 14 | params: 15 | account-number 16 | customer-number -------------------------------------------------------------------------------- /test/acceptance/facade/account-summary.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const debug = require('debug')('loopback:test:account-summary'); 5 | const expect = require('../../support/expect'); 6 | const request = require('request-promise'); 7 | 8 | describe('facade - account summary', () => { 9 | describe('GET /api/Accounts/summary?accountNumber=...', () => { 10 | it('returns the aggregated account info', () => { 11 | const accountNumber = 'CHK52321122'; 12 | return request({ 13 | uri: 'http://localhost:3000/api/Accounts/summary', 14 | qs: { 15 | accountNumber: accountNumber 16 | }, 17 | json: true 18 | }) 19 | .then(res => { 20 | debug(res); 21 | expect(res.account).to.eql({ 22 | accountNumber: accountNumber, 23 | avgBalance: 398.93, 24 | balance: 85.84, 25 | branch: 'Foster City', 26 | customerNumber: '000343223', 27 | minimumBalance: 10, 28 | type: 'Checking', 29 | }); 30 | expect(res.customer).to.containSubset({ 31 | customerNumber: '000343223', 32 | firstName: 'Homer', 33 | lastName: 'Simpson', 34 | }); 35 | expect(res.transactions).to.have.length(5); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/acceptance/facade/index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | require('./account-summary.test'); 5 | require('./vitals.test'); 6 | -------------------------------------------------------------------------------- /test/acceptance/facade/vitals.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const debug = require('debug')('loopback:test:vitals'); 5 | const expect = require('../../support/expect'); 6 | const request = require('request-promise'); 7 | 8 | describe('facade - vitals', () => { 9 | const facadeUrl = 'http://localhost:3000'; 10 | 11 | describe('GET /facade/vitals', () => { 12 | it('returns the current health status of all microservices', () => { 13 | return request({ 14 | uri: facadeUrl + '/api/Vitals', 15 | json: true 16 | }) 17 | .then(res => { 18 | expect(res.status).to.equal('healthy'); 19 | expect(res.dependencies.Account.status).to.equal('healthy'); 20 | expect(res.dependencies.Customer.status).to.equal('healthy'); 21 | expect(res.dependencies.Transaction.status).to.equal('healthy'); 22 | expect(res.dependencies.Account.dependencies).to.deep.equal({ 23 | AccountDB: { status: 'healthy' } }); 24 | expect(res.dependencies.Customer.dependencies).to.deep.equal({ 25 | CustomerDB: { status: 'healthy' } }); 26 | expect(res.dependencies.Transaction.dependencies).to.deep.equal({ 27 | TransactionDB: { status: 'healthy' } }); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('GET /facade/vitals/docker', () => { 33 | it('returns OK', () => { 34 | return request({ 35 | uri: facadeUrl + '/vitals/docker', 36 | json: true 37 | }) 38 | .then(res => { 39 | expect(res).to.equal('ok'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --timeout 15000 3 | -------------------------------------------------------------------------------- /test/support/expect.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-example-facade 3 | 4 | const chai = require('chai'); 5 | const dirtyChai = require('dirty-chai'); 6 | 7 | chai.use(dirtyChai); 8 | chai.use(require('chai-subset')); 9 | 10 | module.exports = chai.expect; 11 | --------------------------------------------------------------------------------