diff --git a/CODEOWNERS b/CODEOWNERS index e8fb812a9df1..0dae87a13ef7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,7 @@ packages/core/* @bajtos @raymondfeng @kjdelisle packages/example-getting-started/* @bajtos @kjdelisle packages/example-hello-world/* @b-admike packages/example-log-extension/* @virkt25 +packages/example-microservices/* @raymondfeng @virkt25 @kjdelisle packages/example-rpc-server/* @kjdelisle packages/metadata/* @raymondfeng packages/openapi-spec/* @bajtos @jannyHou diff --git a/lerna.json b/lerna.json index 05dedc9f7343..5a15c8b0e848 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,9 @@ { "lerna": "2.5.1", - "packages": ["packages/*"], + "packages": [ + "packages/*", + "packages/example-microservices/services/*" + ], "command": { "publish": { "conventionalCommits": true diff --git a/package.json b/package.json index ce2f2becba6e..c412ccfffbc8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "build:full": "npm run clean:lerna && npm run bootstrap && npm run build && npm run mocha && npm run lint", "pretest": "npm run clean && npm run build:current", "test": "node packages/build/bin/run-nyc npm run mocha", - "mocha": "node packages/build/bin/run-mocha \"packages/*/DIST/test/**/*.js\" \"packages/cli/test/*.js\"", + "mocha": "node packages/build/bin/run-mocha \"packages/*/DIST/test/**/*.js\" \"packages/cli/test/*.js\" && node packages/example-microservices/bin/test", "posttest": "npm run lint" }, "config": { diff --git a/packages/cli/generators/example/index.js b/packages/cli/generators/example/index.js index bf96bd6c1c67..31d8663b240c 100644 --- a/packages/cli/generators/example/index.js +++ b/packages/cli/generators/example/index.js @@ -16,6 +16,7 @@ const EXAMPLES = { 'An application and tutorial on how to build with LoopBack 4.', 'hello-world': 'A simple hello-world Application using LoopBack 4', 'log-extension': 'An example extension project for LoopBack 4', + 'microservices': 'How to build scalable microservices using LoopBack', 'rpc-server': 'A basic RPC server using a made-up protocol.', }; Object.freeze(EXAMPLES); diff --git a/packages/example-microservices/LICENSE b/packages/example-microservices/LICENSE new file mode 100644 index 000000000000..363c0a82c3b0 --- /dev/null +++ b/packages/example-microservices/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018. All Rights Reserved. +Node module: @loopback/example-microservices +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/example-microservices/README.md b/packages/example-microservices/README.md new file mode 100644 index 000000000000..39a5a06f1ecf --- /dev/null +++ b/packages/example-microservices/README.md @@ -0,0 +1,57 @@ +# loopback-next-example + +[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/strongloop/loopback) + +How to build scalable microservices using LoopBack.next. + +> What's the difference between LoopBack.next and the current version of +> Loopback? See [LoopBack 3 vs LoopBack 4](https://github.com/strongloop/loopback-next/wiki/FAQ#loopback-3-vs-loopback-4). + +## Installation + +Make sure you have the following installed: + +- [Node.js](https://nodejs.org/en/download/) at v6.x or greater + +1. Install the new loopback CLI toolkit. +``` +npm i -g @loopback/cli +``` + +2. Download the "microservices" example. +``` +lb4 example microservices +``` + +3. Switch to the directory and install dependencies. +``` +cd loopback-example-microservices && npm install +``` + +## Basic use + +```shell +# start all microservices +npm start + +# perform GET request to retrieve account summary data +curl localhost:3000/account/summary?accountNumber=CHK52321122 # or npm test + +# perform GET request to retrieve account data +curl localhost:3001/accounts?accountNumber=CHK52321122 + +# stop all microservices +npm stop +``` + +> Helper scripts for the above commands are in [`/bin`](https://github.com/strongloop/loopback-next-example/tree/master/bin) +directory. + +# Contributing + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing) +- [Join the team](https://github.com/strongloop/loopback-next/wiki/Contributing#join-the-team) + +# License + +MIT diff --git a/packages/example-microservices/bin/get-account b/packages/example-microservices/bin/get-account new file mode 100755 index 000000000000..7f09cc5c57aa --- /dev/null +++ b/packages/example-microservices/bin/get-account @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +curl -s "http://localhost:3001/accounts?accountNumber=CHK52321122" | jq . + diff --git a/packages/example-microservices/bin/get-account-summary b/packages/example-microservices/bin/get-account-summary new file mode 100755 index 000000000000..8f0eab1c7e49 --- /dev/null +++ b/packages/example-microservices/bin/get-account-summary @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -s "http://localhost:3000/account/summary?accountNumber=CHK52321122" | jq . diff --git a/packages/example-microservices/bin/install.js b/packages/example-microservices/bin/install.js new file mode 100755 index 000000000000..707292b09e71 --- /dev/null +++ b/packages/example-microservices/bin/install.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const spawn = require('child_process').spawn; + +const servicesRoot = path.resolve(__dirname, '..', 'services'); +const services = fs.readdirSync(servicesRoot); + +if (process.env.LERNA_ROOT_PATH) { + console.log( + '**Lerna was detected, skipping "npm install" in individual services**' + ); + process.exit(0); +} + +let p = Promise.resolve(); +for (const s of services) { + p = p.then(() => { + return execNpmInstall(`services/${s}`); + }); +} + +p.catch(err => { + console.error(err); + process.exit(1); +}); + +function execNpmInstall(cwd) { + console.log(`\n=== Running "npm install" in ${cwd} ===\n`); + return new Promise((resolve, reject) => { + const child = spawn('npm', ['install'], { + cwd: cwd, + stdio: 'inherit', + // On Windows, `npm` is not an executable filea + // we have to execute it via shell + shell: true, + }); + child.once('error', err => reject(err)); + child.once('exit', (code, signal) => { + if (code || signal) + reject(`npm install failed: exit code ${code} signal ${signal}`); + else + resolve(); + }); + }); +} diff --git a/packages/example-microservices/bin/start b/packages/example-microservices/bin/start new file mode 100755 index 000000000000..aeb75d448918 --- /dev/null +++ b/packages/example-microservices/bin/start @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +./bin/stop + +echo "Starting microservices..." +cd services/account +ts-node index.ts lb-next-account-micro-serv & +echo "Started Account microservice, PID: $!" +cd ../customer && \ +ts-node index.ts lb-next-customer-micro-serv & +echo "Started Customer microservice, PID: $!" +cd ../transaction && \ +ts-node index.ts lb-next-transaction-micro-serv & +echo "Started Transaction microservice, PID: $!" +cd ../facade && \ +ts-node index.ts lb-next-facade-micro-serv & +echo "Started Facade microservice, PID: $!" + +sleep 5 + +echo 'All microservices started successfully.' +echo 'To test the application, run "npm test".' diff --git a/packages/example-microservices/bin/stop b/packages/example-microservices/bin/stop new file mode 100755 index 000000000000..08c3ad336ecd --- /dev/null +++ b/packages/example-microservices/bin/stop @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +ps -ef | grep "[l]b-next" | awk '{print $2}' | xargs kill -9 +echo "All microservices stopped successfully." diff --git a/packages/example-microservices/bin/test.js b/packages/example-microservices/bin/test.js new file mode 100644 index 000000000000..a786e0e101b7 --- /dev/null +++ b/packages/example-microservices/bin/test.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +'use strict'; + +const Promise = require('bluebird'); +const exec = require('child_process').execSync; +const spawn = require('child_process').spawn; +const fs = Promise.promisifyAll(require('fs')); +const path = require('path'); + +const cmd = path.resolve(__dirname, '..', 'node_modules', '.bin', '_mocha'); +const args = ['--compilers', 'ts:ts-node/register,tsx:ts-node/register']; + +const services = path.resolve(__dirname, '..', 'services'); +return fs + .readdirAsync(services) + .then(folders => { + return Promise.each(folders, f => { + const dir = path.resolve(services, f); + return fs + .readdirAsync(dir) + .then(subfolders => { + if (subfolders.indexOf('test') > -1) { + return new Promise((resolve, reject) => { + console.log('RUN TESTS - %s:', f); + const testArgs = args.push(path.resolve(dir, 'test/**/*test.ts')); + const test = spawn(cmd, args, {stdio: 'inherit'}); + test.on('close', code => { + if (code) { + return reject(code); + } else { + console.log('TEST SUCCESS - %s', f); + return resolve(); + } + }); + }); + } else { + console.log('No "test" folder was found in %s', f); + return Promise.resolve(); + } + }) + .catch(code => { + return Promise.reject(`TESTS FAILED - ${f}, exit code ${code}`); + }); + }).then(() => { + console.log('TESTS COMPLETE'); + }); + }) + .catch(err => { + console.log(err); + process.exit(1); + }); diff --git a/packages/example-microservices/package.json b/packages/example-microservices/package.json new file mode 100644 index 000000000000..44cb6bb25c5e --- /dev/null +++ b/packages/example-microservices/package.json @@ -0,0 +1,40 @@ +{ + "name": "@loopback/example-microservices", + "version": "4.0.0-alpha.0", + "description": "How to build scalable microservices using LoopBack", + "private": true, + "main": "facade/index.js", + "scripts": { + "postinstall": "node bin/install.js", + "restart": "npm run stop && npm run start", + "start": "bin/start", + "stop": "bin/stop", + "test": "npm run mocha", + "mocha": "node bin/test.js" + }, + "engines": { + "node": ">=6" + }, + "keywords": [ + "loopback-next", + "example" + ], + "author": "IBM", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/strongloop/loopback-next-example.git" + }, + "bugs": { + "url": "https://github.com/strongloop/loopback-next-example/issues" + }, + "homepage": "https://github.com/strongloop/loopback-next-example#readme", + "dependencies": { + "bluebird": "^3.5.0", + "ts-node": "^3.1.0", + "typescript": "^2.4.1" + }, + "devDependencies": { + "mocha": "^4.0.0" + } +} diff --git a/packages/example-microservices/services/account-without-juggler/README.md b/packages/example-microservices/services/account-without-juggler/README.md new file mode 100644 index 000000000000..b36d61b705bc --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/README.md @@ -0,0 +1,129 @@ +# Account Service with Custom MySQL Datasource and Connector + +## Summary +A REST API for managing bank accounts. + +## Installation +This example requires Node.js 8.x or higher. +Install dependencies: +``` +npm i +``` + +After you've installed your dependencies, you'll need to configure the +`repositories/account/datasources/mysql.json` file to point towards +your MySQL database. + +By default, this example is configured with credentials defined by +loopback-connector-mysql module's `setup.sh` file (which we use for +unit tests). + + +## Overview + +This sample application demonstrates a simple CRUD API for an Account model. +This application does not currently have an explorer component. + +Perform a series of CRUD operations using GET, POST, PATCH and DELETE +on the `localhost:3001/accounts` route! + +In this example app, the AccountRepository is using a custom MySQL connector +implementation in `repositories/account/datasources/mysqlconn.ts` which +has the methods to run corresponding queries in the underlying database +for the CRUD operations without the use of LoopBack's juggler module. + +## Use + +Run the app (from the root directory i.e. inside `account-without-juggler`): +``` +npm start +``` + +Use `cURL` or your favourite REST client to access the endpoints! + +### Create an Account + +`POST /accounts/create` + +Body: +```json +{ +"id": "30", +"customerNumber": "600", +"balance": 220, +"branch": "TO", +"type": "Savings", +"avgBalance": 100, +"minimumBalance": 30 +} +``` + +Returns: +```json +{ +"id": "30", +"customerNumber": "600", +"balance": 220, +"branch": "TO", +"type": "Savings", +"avgBalance": 100, +"minimumBalance": 30 +} +``` + +### Get Account by ID + +`GET /accounts/?filter={"where": {"id": "30"}}` + +Returns: +```json +{ +"id": "30", +"customerNumber": "600", +"balance": 220, +"branch": "TO", +"type": "Savings", +"avgBalance": 100, +"minimumBalance": 30 +} +``` + +You can also filter by other fields by changing the value of the where filter +in the above example. If you specify an empty filter (i.e. `{}`), you will get +all the account instances in the database. + +### Update Account by ID + +`PATCH /accounts/update?id=30` +Body: +```json +{ + "customerNumber": "601" +} +``` + +Returns: +```json +{ + "count": 1 +} +``` + +### Delete an Account by ID + +`DELETE /accounts/delete?id=30` + +Returns: +```json +{ + "count": 1 +} +``` + +## Tests +Run tests with `npm test`! + +## What's Next? +Now that you've got a working example to play with, you can try to +implement your own custom connector using other databases such as +MsSQL or Oracle or improve this example. \ No newline at end of file diff --git a/packages/example-microservices/services/account-without-juggler/bin/get-account b/packages/example-microservices/services/account-without-juggler/bin/get-account new file mode 100755 index 000000000000..2b96d8972efe --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/bin/get-account @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -s localhost:3001/accounts | jq diff --git a/packages/example-microservices/services/account-without-juggler/controllers/AccountController.api.ts b/packages/example-microservices/services/account-without-juggler/controllers/AccountController.api.ts new file mode 100644 index 000000000000..212bd8fc5449 --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/controllers/AccountController.api.ts @@ -0,0 +1,151 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + paths: { + '/accounts': { + get: { + 'x-operation-name': 'getAccount', + parameters: [ + { + name: 'filter', + in: 'query', + description: + 'The criteria used to narrow down the number of accounts returned.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'array', + $ref: '#/definitions/Account', + }, + }, + }, + }, + }, + '/accounts/create': { + post: { + 'x-operation-name': 'createAccount', + parameters: [ + { + name: 'accountInstance', + in: 'body', + description: 'The account instance to create.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + $ref: '#/definitions/Account', + }, + }, + }, + }, + }, + '/accounts/update': { + post: { + 'x-operation-name': 'updateById', + parameters: [ + { + name: 'id', + in: 'query', + description: 'The id of the model instance to be updated.', + required: true, + type: 'string', + }, + { + name: 'data', + in: 'body', + description: 'An object of model property name/value pairs.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'object', + description: 'Information about the updated record.', + properties: { + count: { + type: 'number', + description: 'The number of records updated.', + }, + }, + }, + }, + }, + }, + }, + '/accounts/delete': { + delete: { + 'x-operation-name': 'deleteById', + parameters: [ + { + name: 'id', + in: 'query', + description: 'The ID for the model instance to be deleted.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'object', + description: 'Information on the deleted record.', + properties: { + count: { + type: 'number', + description: 'The number of records deleted.', + }, + }, + }, + }, + }, + }, + }, + }, + definitions: { + Account: { + properties: { + id: { + type: 'string', + description: 'The ID for the account instance.', + }, + customerNumber: { + type: 'string', + description: 'The customer ID for the account instance.', + }, + balance: { + type: 'number', + description: 'The current balance for the account instance.', + }, + branch: { + type: 'string', + description: 'The branch location for the account instance.', + }, + type: { + type: 'string', + description: 'The type of banking account.', + }, + avgBalance: { + type: 'number', + description: 'The average balance for the account instance.', + }, + minimumBalance: { + type: 'number', + description: 'The minimum balance for the account instance.', + }, + }, + }, + }, +}; diff --git a/packages/example-microservices/services/account-without-juggler/controllers/AccountController.ts b/packages/example-microservices/services/account-without-juggler/controllers/AccountController.ts new file mode 100644 index 000000000000..8c3e8a29016d --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/controllers/AccountController.ts @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {api} from '@loopback/rest'; +import {def} from './AccountController.api'; +import {AccountRepository} from '../repositories/account'; +import {inject} from '@loopback/core'; +import {Account} from '../repositories/account/models/Account'; + +@api(def) +export class AccountController { + constructor( + @inject('repositories.account') private repository: AccountRepository, + ) {} + + //fixme figure out how to use Filter interface + //fixme filter is string even though swagger spec + //defines it as object type + async getAccount(filter: string): Promise { + return await this.repository.find(JSON.parse(filter)); + } + + async createAccount(accountInstance: Object): Promise { + return await this.repository.create(accountInstance); + } + + async updateById(id: string, data: Object) { + return await this.repository.updateById(id, data); + } + + async deleteById(id: string) { + return await this.repository.deleteById(id); + } +} diff --git a/packages/example-microservices/services/account-without-juggler/index.ts b/packages/example-microservices/services/account-without-juggler/index.ts new file mode 100644 index 000000000000..f84ca8fd313a --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/index.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {AccountController} from './controllers/AccountController'; +import {AccountRepository} from './repositories/account'; + +class AccountMicroservice extends Application { + private _startTime: Date; + + constructor() { + super(); + + const app = this; + app.controller(AccountController); + app.bind('http.port').to(3001); + app.bind('repositories.account').toClass(AccountRepository); + } + + async start() { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return { + appName: 'account-without-juggler', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }; + } +} + +async function main(): Promise { + const app = new AccountMicroservice(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/account-without-juggler/package.json b/packages/example-microservices/services/account-without-juggler/package.json new file mode 100644 index 000000000000..9d02897473ae --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/package.json @@ -0,0 +1,41 @@ +{ + "name": "@loopback-example-microservices/account-without-juggler", + "version": "1.0.0", + "description": "The Account microservice.", + "private": true, + "main": "index.ts", + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "dependencies": { + "@loopback/core": "^4.0.0-alpha.10", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "loopback-datasource-juggler": "^3.4.1", + "mysql": "^2.13.0", + "mysql-promise": "^4.1.0" + }, + "devDependencies": { + "@loopback/testlab": "^4.0.0-alpha.6", + "@types/node": "^7.0.12", + "debug": "^2.6.8", + "lodash": "^4.17.4", + "mocha": "^3.4.2", + "ts-node": "^3.0.4", + "tslint": "^5.4.3", + "typescript": "^2.4.1" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "mocha --opts test/mocha.opts 'test/unit/*.ts'" + }, + "keywords": [ + "loopback-next", + "example", + "account", + "microservice" + ], + "author": "IBM", + "license": "MIT" +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysql.json b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysql.json new file mode 100644 index 000000000000..2a9e75938555 --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysql.json @@ -0,0 +1,7 @@ +{ + "host":"localhost", + "user":"root", + "password": "pass", + "database": "testdb", + "port": 3306 +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlconn.ts b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlconn.ts new file mode 100644 index 000000000000..f6d1732e96ef --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlconn.ts @@ -0,0 +1,215 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// tslint:disable:no-any + +const debug = require('debug')( + 'loopback:repositories:account:datasources:connections:mysql', +); +const mysql = require('mysql'); +const db = require('mysql-promise')(); +import { + Class, + CrudConnector, + DataSource, + Entity, + EntityData, + Filter, + ObjectType, + Options, + Where, +} from '@loopback/repository'; + +export class MySqlConn implements CrudConnector { + //fixme make connection strongly typed + private connection: any; + + constructor(config: Object) { + db.configure(config, mysql); + this.connection = db; + } + name: 'mysql'; + interfaces?: string[]; + connect(): Promise { + return this.connection.connect(); + } + disconnect(): Promise { + return this.connection.end(); + } + ping(): Promise { + return this.connection.ping(); + } + + updateAll( + modelClass: Class, + data: EntityData, + where: Where, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + create( + modelClass: Class, + entity: EntityData, + options: Options, + ): Promise { + const self = this; + const placeHolders = []; + for (const prop in modelClass.definition.properties) { + placeHolders.push('?'); + } + const createQuery = + 'INSERT INTO ?? VALUES (' + placeHolders.join(',') + ')'; + const vals = [modelClass.modelName]; + for (const prop in entity) { + vals.push(entity[prop]); + } + const sqlStmt = mysql.format(createQuery, vals); + debug('Insert ', sqlStmt); + + return self.connection.query(sqlStmt).spread(function(result: any) { + if (result) { + //MySQL returns count of affected rows, but as part of our API + //definition, we return the instance we used to create the row + return entity; + } + }); + } + + save( + modelClass: Class, + entity: EntityData, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + find( + modelClass: Class, + filter: Filter, + options: Options, + ): Promise { + const self = this; + let findQuery = 'SELECT * FROM ?? '; + findQuery = mysql.format(findQuery, [modelClass.modelName]); + if (filter.where) { + let whereClause = '?? = ?'; + for (const key in filter.where) { + whereClause = mysql.format(whereClause, [key, filter.where[key]]); + } + findQuery += ' WHERE ' + whereClause; + } + debug('Find ', findQuery); + return self.connection.query(findQuery).spread(function(rows: any) { + return rows; + }); + } + + findById( + modelClass: Class, + id: any, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + update( + modelClass: Class, + entity: EntityData, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + delete( + modelClass: Class, + entity: EntityData, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + createAll( + modelClass: Class, + entities: EntityData[], + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + updateById( + modelClass: Class, + id: any, + data: EntityData, + options: Options, + ): Promise { + const self = this; + let updateQuery = 'UPDATE ?? SET '; + updateQuery = mysql.format(updateQuery, [modelClass.modelName]); + const updateClause = []; + for (const prop in data) { + updateClause.push(mysql.format('??=?', [prop, data[prop]])); + } + updateQuery += updateClause.join(','); + const whereClause = mysql.format(' WHERE ??=?', ['id', id]); + updateQuery += whereClause; + + debug('updateById ', updateQuery); + return self.connection.query(updateQuery).spread(function(result: any) { + return result.affectedRows; + }); + } + + replaceById( + modelClass: Class, + id: any, + data: EntityData, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + deleteAll( + modelClass: Class, + where: Where, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + deleteById( + modelClass: Class, + id: any, + options: Options, + ): Promise { + const self = this; + let deleteQuery = 'DELETE FROM ?? '; + deleteQuery = mysql.format(deleteQuery, modelClass.modelName); + const whereClause = mysql.format(' WHERE ??=?', ['id', id]); + deleteQuery += whereClause; + + debug('deleteById ', deleteQuery); + return self.connection.query(deleteQuery).spread(function(result: any) { + return result.affectedRows; + }); + } + + count( + modelClass: Class, + where: Where, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } + + exists( + modelClass: Class, + id: any, + options: Options, + ): Promise { + throw new Error('Not implemented yet.'); + } +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlds.ts b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlds.ts new file mode 100644 index 000000000000..66c21ddd1029 --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/datasources/mysqlds.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {MySqlConn} from './mysqlconn'; +import {DataSource} from '@loopback/repository'; +const mysqlCreds = require('./mysql.json'); + +export class MySqlDs implements DataSource { + name: 'mysqlDs'; + connector: MySqlConn; + settings: Object; + + constructor() { + this.settings = mysqlCreds; + this.connector = new MySqlConn(this.settings); + } +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/index.ts b/packages/example-microservices/services/account-without-juggler/repositories/account/index.ts new file mode 100644 index 000000000000..71c98c30e6be --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/index.ts @@ -0,0 +1,15 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {CrudRepositoryImpl} from '@loopback/repository'; +import {MySqlDs} from './datasources/mysqlds'; +import {Account} from './models/Account'; + +export class AccountRepository extends CrudRepositoryImpl { + constructor() { + const ds = new MySqlDs(); + super(ds, Account); + } +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/models/Account.ts b/packages/example-microservices/services/account-without-juggler/repositories/account/models/Account.ts new file mode 100644 index 000000000000..78c42bd9ed6b --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/models/Account.ts @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Entity, + model, + ModelDefinition, + PropertyDefinition, +} from '@loopback/repository'; + +const definition = require('./account/model-definition'); +@model(definition) +export class Account extends Entity { + static definition = new ModelDefinition(definition); + static modelName = 'Account'; + + id: string; + customerNumber: string; + balance: number; + branch: string; + type: string; + avgBalance: number; + minimumBalance: number; + + constructor(body?: Partial) { + super(); + if (body) { + Object.assign(this, body); + } + } +} diff --git a/packages/example-microservices/services/account-without-juggler/repositories/account/models/account/model-definition.js b/packages/example-microservices/services/account-without-juggler/repositories/account/models/account/model-definition.js new file mode 100644 index 000000000000..6b55b9c7e5bf --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/repositories/account/models/account/model-definition.js @@ -0,0 +1,39 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = { + name: 'Account', + properties: { + id: { + type: 'string', + required: true, + id: true + }, + customerNumber: { + type: 'string', + required: true + }, + balance: { + type: 'number', + required: true + }, + branch: { + type: 'string', + required: true + }, + type: { + type: 'string', + required: true + }, + avgBalance: { + type: 'number', + required: true + }, + minimumBalance: { + type: 'number', + required: true + } + } +}; diff --git a/packages/example-microservices/services/account-without-juggler/test/mocha.opts b/packages/example-microservices/services/account-without-juggler/test/mocha.opts new file mode 100644 index 000000000000..e6331ca64c27 --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/test/mocha.opts @@ -0,0 +1,2 @@ +--compilers ts:ts-node/register +--recursive diff --git a/packages/example-microservices/services/account-without-juggler/test/unit/account-controller.test.ts b/packages/example-microservices/services/account-without-juggler/test/unit/account-controller.test.ts new file mode 100644 index 000000000000..aa98a580731a --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/test/unit/account-controller.test.ts @@ -0,0 +1,84 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import 'mocha'; +import {AccountController} from '../../controllers/AccountController'; +import {expect} from '@loopback/testlab'; +import {AccountRepository} from '../../repositories/account'; + +let testController: AccountController; + +const testAcc = { + id: 'test1', + customerNumber: '1234', + balance: 1000, + branch: 'Toronto', + type: 'Chequing', + avgBalance: 500, + minimumBalance: 0, +}; + +const brokenAcc = { + customerNumber: '123456', + balance: 1000, + branch: 'Broke City', + type: 'Chequing', +}; + +// NOTE(bajtos) These tests require a MySQL database running on localhost +// Our CI setup is not ready for that yet, so let's skip these tests for now. +describe.skip('AccountController Unit Test Suite', () => { + before(createAccountController); + + it('creates an account instance', async () => { + const result = await testController.createAccount(testAcc); + expect(result).to.deepEqual(testAcc); + const getResult = await testController.getAccount( + '{"where":{"id":"test1"}}', + ); + expect(getResult).to.not.be.empty(); + expect(getResult).have.lengthOf(1); + expect(getResult[0]).to.deepEqual(testAcc); + }); + it('should not create an invalid instance', async () => { + try { + await testController.createAccount(brokenAcc); + } catch (err) { + expect(err).to.not.be.empty(); + } + }); + it('should not accept invalid args', async () => { + try { + await testController.getAccount(''); + } catch (err) { + expect(err).to.not.be.empty(); + } + }); + + it('updates an account instance', async () => { + const result = await testController.updateById('test1', {balance: 2000}); + expect(result).to.equal(true); + const getResult = await testController.getAccount( + '{"where":{"id":"test1"}}', + ); + expect(getResult).to.not.be.empty(); + expect(getResult).have.lengthOf(1); + expect(getResult[0].id).to.be.equal(testAcc.id); + expect(getResult[0].toObject().balance).to.be.equal(2000); + }); + + it('deletes an account instance', async () => { + const result = await testController.deleteById('test1'); + expect(result).to.equal(true); + const getResult = await testController.getAccount( + '{"where":{"id":"test1"}}', + ); + expect(getResult).to.be.empty(); + }); +}); + +function createAccountController() { + testController = new AccountController(new AccountRepository()); +} diff --git a/packages/example-microservices/services/account-without-juggler/tsconfig.common.json b/packages/example-microservices/services/account-without-juggler/tsconfig.common.json new file mode 100644 index 000000000000..1d8e30dea1ea --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/tsconfig.common.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitAny": true, + "strictNullChecks": true, + + "lib": ["es2017", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "sourceMap": true, + "declaration": true + } +} diff --git a/packages/example-microservices/services/account-without-juggler/tsconfig.json b/packages/example-microservices/services/account-without-juggler/tsconfig.json new file mode 100644 index 000000000000..81928c0155b1 --- /dev/null +++ b/packages/example-microservices/services/account-without-juggler/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "./tsconfig.common.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": [ + "node_modules/@types" + ], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} diff --git a/packages/example-microservices/services/account/bin/create-account b/packages/example-microservices/services/account/bin/create-account new file mode 100755 index 000000000000..2626106f872b --- /dev/null +++ b/packages/example-microservices/services/account/bin/create-account @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -H "Content-Type: application/json" -X POST -d '{"id":"test1", "customerNumber": "1234", "balance":1000, "branch":"Toronto", "type":"Chequing", "avgBalance":500, "minimumBalance":0}' 'http://localhost:3001/accounts/create' | jq diff --git a/packages/example-microservices/services/account/bin/delete-account b/packages/example-microservices/services/account/bin/delete-account new file mode 100755 index 000000000000..feac770cf061 --- /dev/null +++ b/packages/example-microservices/services/account/bin/delete-account @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -g -H "Content-Type: application/json" -X DELETE 'http://localhost:3001/accounts/delete?where={"id":"test1"}' | jq diff --git a/packages/example-microservices/services/account/bin/get-account b/packages/example-microservices/services/account/bin/get-account new file mode 100755 index 000000000000..7a85db2be743 --- /dev/null +++ b/packages/example-microservices/services/account/bin/get-account @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -g 'http://localhost:3001/accounts?filter={"where":{"id":"test1"}}' | jq diff --git a/packages/example-microservices/services/account/bin/update-account b/packages/example-microservices/services/account/bin/update-account new file mode 100755 index 000000000000..143f8f368e17 --- /dev/null +++ b/packages/example-microservices/services/account/bin/update-account @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -g -H "Content-Type: application/json" -X POST -d '{"balance":2000}' 'http://localhost:3001/accounts/update?where={"id":"test1"}' | jq diff --git a/packages/example-microservices/services/account/controllers/AccountController.api.ts b/packages/example-microservices/services/account/controllers/AccountController.api.ts new file mode 100644 index 000000000000..ca696c383a40 --- /dev/null +++ b/packages/example-microservices/services/account/controllers/AccountController.api.ts @@ -0,0 +1,122 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + paths: { + '/accounts': { + get: { + 'x-operation-name': 'getAccount', + parameters: [ + { + name: 'filter', + in: 'query', + description: + 'The criteria used to narrow down the number of accounts returned.', + required: false, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'array', + items: '#/definitions/Account', + }, + }, + }, + }, + }, + '/accounts/create': { + post: { + 'x-operation-name': 'createAccount', + parameters: [ + { + name: 'accountInstance', + in: 'body', + description: 'The account instance to create.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + accountInstance: '#/definitions/Account', + }, + }, + }, + }, + }, + '/accounts/update': { + post: { + 'x-operation-name': 'updateAccount', + parameters: [ + { + name: 'where', + in: 'query', + description: + 'The criteria used to narrow down the number of accounts returned.', + required: false, + type: 'object', + }, + { + name: 'data', + in: 'body', + description: 'An object of model property name/value pairs', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'object', + description: 'update information', + properties: { + count: { + type: 'number', + description: 'number of records updated', + }, + }, + }, + }, + }, + }, + }, + '/accounts/delete': { + delete: { + 'x-operation-name': 'deleteAccount', + parameters: [ + { + name: 'where', + in: 'query', + description: + 'The criteria used to narrow down which account instances to delete.', + required: true, + type: 'object', + }, + ], + responses: { + 200: { + schema: { + type: 'object', + description: 'delete information', + properties: { + count: { + type: 'number', + description: 'number of records deleted', + }, + }, + }, + }, + }, + }, + }, + }, + definitions: { + Account: require('../repositories/account/models/account/model-definition.json'), + }, +}; diff --git a/packages/example-microservices/services/account/controllers/AccountController.ts b/packages/example-microservices/services/account/controllers/AccountController.ts new file mode 100644 index 000000000000..4990f6f40d04 --- /dev/null +++ b/packages/example-microservices/services/account/controllers/AccountController.ts @@ -0,0 +1,36 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Filter, Where} from '@loopback/repository'; +import {api} from '@loopback/rest'; +import {def} from './AccountController.api'; +import {AccountRepository} from '../repositories/account'; + +// tslint:disable:no-any + +@api(def) +export class AccountController { + repository: AccountRepository; + + constructor() { + this.repository = new AccountRepository(); + } + + async getAccount(filter: string) { + return await this.repository.find(JSON.parse(filter)); + } + + async createAccount(accountInstance: any) { + return await this.repository.create(accountInstance); + } + + async updateAccount(where: string, data: any) { + return await this.repository.update(JSON.parse(where), data); + } + + async deleteAccount(where: string) { + return await this.repository.deleteAccount(JSON.parse(where)); + } +} diff --git a/packages/example-microservices/services/account/index.ts b/packages/example-microservices/services/account/index.ts new file mode 100644 index 000000000000..0a19c955979e --- /dev/null +++ b/packages/example-microservices/services/account/index.ts @@ -0,0 +1,44 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {AccountController} from './controllers/AccountController'; + +class AccountMicroservice extends Application { + private _startTime: Date; + + constructor() { + super(); + const app = this; + app.bind('http.port').to(3001); + app.controller(AccountController); + } + + async start() { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return { + appName: 'account', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }; + } +} + +async function main(): Promise { + const app = new AccountMicroservice(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/account/package.json b/packages/example-microservices/services/account/package.json new file mode 100644 index 000000000000..662324444ede --- /dev/null +++ b/packages/example-microservices/services/account/package.json @@ -0,0 +1,38 @@ +{ + "name": "@loopback-example-microservices/account", + "version": "1.0.0", + "description": "The Account microservice.", + "private": true, + "main": "index.ts", + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "dependencies": { + "@loopback/core": "^4.0.0-alpha.10", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "loopback-connector-mysql": "^4.2.0", + "loopback-datasource-juggler": "^3.4.1", + "ts-node": "^3.1.0", + "typescript": "^2.3.4" + }, + "devDependencies": { + "@loopback/testlab": "^4.0.0-alpha.6", + "@types/node": "^7.0.12", + "mocha": "^3.4.2", + "@types/mocha": "^2.2.41" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "mocha --opts test/mocha.opts 'test/unit/*.ts'" + }, + "keywords": [ + "loopback-next", + "example", + "account", + "microservice" + ], + "author": "IBM", + "license": "MIT" +} diff --git a/packages/example-microservices/services/account/repositories/account/datasources/local-fs/data.json b/packages/example-microservices/services/account/repositories/account/datasources/local-fs/data.json new file mode 100644 index 000000000000..91fbc61d791b --- /dev/null +++ b/packages/example-microservices/services/account/repositories/account/datasources/local-fs/data.json @@ -0,0 +1,13 @@ +{ + "ids": { + "Account": 4 + }, + "models": { + "Account": { + "CHK52321122": "{\"id\":\"CHK52321122\",\"customerNumber\":\"000343223\",\"balance\":85.84,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":398.93,\"minimumBalance\":10}", + "CHK54520000": "{\"id\":\"CHK54520000\",\"customerNumber\":\"003499223\",\"balance\":99.99,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":500.93,\"minimumBalance\":10}", + "CHK52321199": "{\"id\":\"CHK52321199\",\"customerNumber\":\"000343223\",\"balance\":109.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}", + "CHK99999999": "{\"id\":\"CHK99999999\",\"customerNumber\":\"0002444422\",\"balance\":100.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}" + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/account/repositories/account/index.ts b/packages/example-microservices/services/account/repositories/account/index.ts new file mode 100644 index 000000000000..1d80fffe60b6 --- /dev/null +++ b/packages/example-microservices/services/account/repositories/account/index.ts @@ -0,0 +1,38 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; +const modelDefinition = require('./models/account/model-definition.json'); + +// tslint:disable:no-any + +export class AccountRepository { + model: any; + + constructor(file?: string) { + const ds: juggler.DataSource = new DataSourceConstructor('local-fs', { + connector: 'memory', + file: file || './repositories/account/datasources/local-fs/data.json', + }); + + this.model = ds.createModel('Account', modelDefinition.properties, {}); + } + + async find(filter: any): Promise { + return await this.model.find(filter); + } + + async create(accountInstance: any): Promise { + return await this.model.create(accountInstance); + } + + async update(where: any, data: any): Promise { + return await this.model.updateAll(where, data, {}); + } + + async deleteAccount(where: any): Promise { + return await this.model.destroyAll(where); + } +} diff --git a/packages/example-microservices/services/account/repositories/account/models/account/model-definition.json b/packages/example-microservices/services/account/repositories/account/models/account/model-definition.json new file mode 100644 index 000000000000..dc4ffad065c9 --- /dev/null +++ b/packages/example-microservices/services/account/repositories/account/models/account/model-definition.json @@ -0,0 +1,34 @@ +{ + "name": "Account", + "properties": { + "id": { + "type": "string", + "required": true, + "id": true + }, + "customerNumber": { + "type": "string", + "required": true + }, + "balance": { + "type": "number", + "required": true + }, + "branch": { + "type": "string", + "required": true + }, + "type": { + "type": "string", + "required": true + }, + "avgBalance": { + "type": "number", + "required": true + }, + "minimumBalance": { + "type": "number", + "required": true + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/account/test/mocha.opts b/packages/example-microservices/services/account/test/mocha.opts new file mode 100644 index 000000000000..e6331ca64c27 --- /dev/null +++ b/packages/example-microservices/services/account/test/mocha.opts @@ -0,0 +1,2 @@ +--compilers ts:ts-node/register +--recursive diff --git a/packages/example-microservices/services/account/test/unit/test.data.json b/packages/example-microservices/services/account/test/unit/test.data.json new file mode 100644 index 000000000000..e63da31c27ab --- /dev/null +++ b/packages/example-microservices/services/account/test/unit/test.data.json @@ -0,0 +1,13 @@ +{ + "ids": { + "Account": 34 + }, + "models": { + "Account": { + "CHK52321122": "{\"id\":\"CHK52321122\",\"customerNumber\":\"000343223\",\"balance\":85.84,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":398.93,\"minimumBalance\":10}", + "CHK54520000": "{\"id\":\"CHK54520000\",\"customerNumber\":\"003499223\",\"balance\":99.99,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":500.93,\"minimumBalance\":10}", + "CHK52321199": "{\"id\":\"CHK52321199\",\"customerNumber\":\"000343223\",\"balance\":109.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}", + "CHK99999999": "{\"id\":\"CHK99999999\",\"customerNumber\":\"0002444422\",\"balance\":100.89,\"branch\":\"Foster City\",\"type\":\"Checking\",\"avgBalance\":100.93,\"minimumBalance\":10}" + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/account/test/unit/test.ts b/packages/example-microservices/services/account/test/unit/test.ts new file mode 100644 index 000000000000..bc2f3f9c495a --- /dev/null +++ b/packages/example-microservices/services/account/test/unit/test.ts @@ -0,0 +1,150 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// test/unit/test.js +import 'mocha'; +import {AccountController} from '../../controllers/AccountController'; +import {expect} from '@loopback/testlab'; +import {AccountRepository} from '../../repositories/account'; +import * as path from 'path'; + +let accCtrl: AccountController; + +const testAcc = { + id: 'test1', + customerNumber: '1234', + balance: 1000, + branch: 'Toronto', + type: 'Chequing', + avgBalance: 500, + minimumBalance: 0, +}; + +const brokenAcc = { + customerNumber: '123456', + balance: 1000, + branch: 'Broke City', + type: 'Chequing', +}; + +describe('AccountController Unit Test Suite', () => { + before(createAccountController); + + describe('AccountController.getAccount("{}")', () => { + it('returns an array of all accounts', async () => { + const result = await accCtrl.getAccount('{}'); + expect(result).to.not.be.empty(); + expect(result).have.lengthOf(4); + expect(result[0].id).to.equalOneOf([ + 'CHK52321122', + 'CHK54520000', + 'CHK52321199', + 'CHK99999999', + ]); + }); + }); + + describe('AccountController.getAccount("")', () => { + it('rejects promise for invalid args', async () => { + let flag = true; + try { + await accCtrl.getAccount(''); + } catch (err) { + flag = false; + } + expect(flag).to.be.false(); + }); + }); + + describe('AccountController.getAccount("{"where":{"id":"test1"}}")', () => { + it('searches and returns an empty array', async () => { + const result = await accCtrl.getAccount('{"where":{"id":"test1"}}'); + expect(result).to.be.empty(); + }); + }); + + describe('AccountController.createAccount(testAcc)', () => { + it('should create an account', async () => { + const result = await accCtrl.createAccount(testAcc); + expect(JSON.stringify(result)).to.equal(JSON.stringify(testAcc)); + }); + }); + + describe('AccountController.getAccount("{"where":{"id":"test1"}}")', () => { + it('searches and returns newly created account', async () => { + const result = await accCtrl.getAccount('{"where":{"id":"test1"}}'); + expect(result).to.not.be.empty(); + expect(result).have.lengthOf(1); + expect(result[0].id).to.be.equal(testAcc.id); + }); + }); + + describe('AccountController.createAccount(brokenAcc)', () => { + it('fails to create with an Invalid Account instance.', async () => { + let works = true; + try { + await accCtrl.createAccount(brokenAcc); + } catch (err) { + works = false; + } + expect(works).to.be.false(); + }); + }); + + describe('AccountController.updateAccount("{"id":"test1"}", {"balance":2000})', () => { + it('updates an Account instance', async () => { + const result = await accCtrl.updateAccount('{"id":"test1"}', { + balance: 2000, + }); + expect(result.count).to.be.equal(1); + }); + }); + + describe('AccountController.getAccount("{"where":{"id":"test1"}}")', () => { + it('returns account with updated balance', async () => { + const result = await accCtrl.getAccount('{"where":{"id":"test1"}}'); + expect(result).to.not.be.empty(); + expect(result).have.lengthOf(1); + expect(result[0].id).to.be.equal(testAcc.id); + expect(JSON.parse(JSON.stringify(result[0])).balance).to.be.equal(2000); + }); + }); + + describe('AccountController.deleteAccount("{"id":"test1"}")', () => { + it('deletes the Account instance', async () => { + const result = await accCtrl.deleteAccount('{"id":"test1"}'); + expect(result.count).to.be.equal(1); + }); + }); + + describe('AccountController.getAccount("{"where":{"id":"test1"}}")', () => { + it('searches and returns an empty array', async () => { + const result = await accCtrl.getAccount('{"where":{"id":"test1"}}'); + expect(result).to.be.empty(); + }); + }); + + describe('AccountController.getAccount("{}")', () => { + it('returns an array of all accounts', async () => { + const result = await accCtrl.getAccount('{}'); + expect(result).to.not.be.empty(); + expect(result).have.lengthOf(4); + expect(result[0].id).to.equalOneOf([ + 'CHK52321122', + 'CHK54520000', + 'CHK52321199', + 'CHK99999999', + ]); + }); + }); +}); + +function createAccountController() { + accCtrl = new AccountController(); + + accCtrl.repository = new AccountRepository( + path.resolve(__dirname, 'test.data.json'), + ); +} diff --git a/packages/example-microservices/services/account/tsconfig.json b/packages/example-microservices/services/account/tsconfig.json new file mode 100644 index 000000000000..741c22be99a3 --- /dev/null +++ b/packages/example-microservices/services/account/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": [ + "node_modules/@types" + ], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/example-microservices/services/customer/bin/get-customers b/packages/example-microservices/services/customer/bin/get-customers new file mode 100755 index 000000000000..e54e10272d76 --- /dev/null +++ b/packages/example-microservices/services/customer/bin/get-customers @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -s localhost:3002/customers | jq \ No newline at end of file diff --git a/packages/example-microservices/services/customer/controllers/CustomerController.api.ts b/packages/example-microservices/services/customer/controllers/CustomerController.api.ts new file mode 100644 index 000000000000..33333bd7d5c4 --- /dev/null +++ b/packages/example-microservices/services/customer/controllers/CustomerController.api.ts @@ -0,0 +1,81 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + paths: { + '/customers': { + get: { + 'x-operation-name': 'getCustomers', + parameters: [ + { + name: 'id', + in: 'query', + description: 'The customer id.', + required: true, + type: 'string', + format: 'JSON', + }, + { + name: 'filter', + in: 'query', + description: + 'The criteria used to narrow down the number of customers returned.', + required: false, + type: 'string', + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + customerNumber: { + type: 'string', + description: 'The customer number.', + }, + firstName: { + type: 'string', + description: "The customer's first name.", + }, + lastName: { + type: 'string', + description: "The customer's last name.", + }, + ssn: { + type: 'string', + description: "The customer's social security number.", + }, + customerSince: { + type: 'datetime', + description: "The customer's registration date.", + }, + street: { + type: 'string', + description: "The street name of the customer's address.", + }, + state: { + type: 'string', + description: "The state of the customer's address.", + }, + city: { + type: 'string', + description: "The city of the customer's address.", + }, + zip: { + type: 'string', + description: "The zip code of the customer's address.", + }, + lastUpdated: { + type: 'string', + description: + "The last time the customer's information was updated.", + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/example-microservices/services/customer/controllers/CustomerController.ts b/packages/example-microservices/services/customer/controllers/CustomerController.ts new file mode 100644 index 000000000000..1e75462853ed --- /dev/null +++ b/packages/example-microservices/services/customer/controllers/CustomerController.ts @@ -0,0 +1,23 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {api} from '@loopback/rest'; +import {def} from './CustomerController.api'; +import {CustomerRepository} from '../repositories/customer'; + +// tslint:disable:no-any + +@api(def) +export class CustomerController { + repository: CustomerRepository; + + constructor() { + this.repository = new CustomerRepository(); + } + + async getCustomers(filter): Promise { + return await this.repository.find(filter); + } +} diff --git a/packages/example-microservices/services/customer/index.ts b/packages/example-microservices/services/customer/index.ts new file mode 100644 index 000000000000..40d5dd5384fe --- /dev/null +++ b/packages/example-microservices/services/customer/index.ts @@ -0,0 +1,44 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {CustomerController} from './controllers/CustomerController'; + +class CustomerApplication extends Application { + private _startTime: Date; + + constructor() { + super(); + const app = this; + app.controller(CustomerController); + app.bind('http.port').to(3002); + } + + async start() { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return { + appName: 'customer', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }; + } +} + +async function main(): Promise { + const app = new CustomerApplication(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/customer/package.json b/packages/example-microservices/services/customer/package.json new file mode 100644 index 000000000000..6ae35f8f030e --- /dev/null +++ b/packages/example-microservices/services/customer/package.json @@ -0,0 +1,32 @@ +{ + "name": "@loopback-example-microservices/customer", + "version": "1.0.0", + "description": "The Customer microservice.", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "main": "index.ts", + "dependencies": { + "@loopback/core": "^4.0.0-alpha.10", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "loopback-datasource-juggler": "^3.4.1" + }, + "devDependencies": { + "@types/node": "^7.0.12" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "loopback-next", + "example", + "customer", + "microservice" + ], + "author": "IBM", + "license": "MIT" +} diff --git a/packages/example-microservices/services/customer/repositories/customer/datasources/local-fs/data.json b/packages/example-microservices/services/customer/repositories/customer/datasources/local-fs/data.json new file mode 100644 index 000000000000..2059b6d325c6 --- /dev/null +++ b/packages/example-microservices/services/customer/repositories/customer/datasources/local-fs/data.json @@ -0,0 +1,11 @@ +{ + "ids": { + "Customer": 2 + }, + "models": { + "Customer": { + "000343223": "{\"id\":\"000343223\",\"firstName\":\"Ron\",\"lastName\":\"Simpson\",\"ssn\":\"141-XX-X800\",\"customerSince\":\"2017-03-14T23:05:18.779Z\",\"street\":\"742 Evergreen Terrace\",\"state\":\"OR\",\"city\":\"Springfield\",\"zip\":\"95555\",\"lastUpdated\":\"2017-03-14T23:05:18.599Z\"}", + "003499223": "{\"id\":\"003499223\",\"firstName\":\"Raymond\",\"lastName\":\"Freerich\",\"ssn\":\"342-XX-XX24\",\"customerSince\":\"2017-03-14T23:26:45.225Z\",\"street\":\"3005 chapel ave\",\"state\":\"NewJersey\",\"city\":\"Chapel Ave\",\"zip\":\"08002\",\"lastUpdated\":\"2017-03-14T23:26:45.225Z\"}" + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/customer/repositories/customer/index.ts b/packages/example-microservices/services/customer/repositories/customer/index.ts new file mode 100644 index 000000000000..2985a78d581d --- /dev/null +++ b/packages/example-microservices/services/customer/repositories/customer/index.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; +const modelDefinition = require('./models/customer/model-definition.json'); + +// tslint:disable:no-any + +export class CustomerRepository { + model; + + constructor() { + const ds: juggler.DataSource = new DataSourceConstructor('local-fs', { + connector: 'memory', + file: './repositories/customer/datasources/local-fs/data.json', + }); + this.model = ds.createModel('Customer', modelDefinition); + } + + async find(id): Promise { + return await this.model.find({where: {id: id}}); + } +} diff --git a/packages/example-microservices/services/customer/repositories/customer/models/customer/model-definition.json b/packages/example-microservices/services/customer/repositories/customer/models/customer/model-definition.json new file mode 100644 index 000000000000..9325ca4c326e --- /dev/null +++ b/packages/example-microservices/services/customer/repositories/customer/models/customer/model-definition.json @@ -0,0 +1,37 @@ +{ + "name": "Customer", + "properties": { + "id": { + "type": "string", + "id": true, + "required": true + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "ssn": { + "type": "string" + }, + "customerSince": { + "type": "date" + }, + "street": { + "type": "string" + }, + "state": { + "type": "string" + }, + "city": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "lastUpdated": { + "type": "date" + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/customer/tsconfig.json b/packages/example-microservices/services/customer/tsconfig.json new file mode 100644 index 000000000000..741c22be99a3 --- /dev/null +++ b/packages/example-microservices/services/customer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": [ + "node_modules/@types" + ], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/example-microservices/services/facade/controllers/AccountManagementController.api.ts b/packages/example-microservices/services/facade/controllers/AccountManagementController.api.ts new file mode 100644 index 000000000000..4eb594e94a54 --- /dev/null +++ b/packages/example-microservices/services/facade/controllers/AccountManagementController.api.ts @@ -0,0 +1,159 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + paths: { + '/account/summary': { + get: { + 'x-operation-name': 'getSummary', + parameters: [ + { + name: 'accountNumber', + in: 'query', + description: + 'The account number to use when retrieving data from the underlying microservices.', + required: true, + type: 'string', + }, + ], + responses: { + 200: { + schema: { + accounts: { + type: 'object', + properties: { + accountNumber: { + type: 'string', + description: 'account number', + }, + customerNumber: { + type: 'string', + description: 'customer number', + }, + type: { + type: 'string', + description: 'savings or checking', + }, + balance: { + type: 'number', + description: 'balance amount', + }, + minimumBalance: { + type: 'number', + description: 'account minimum balance', + }, + avgBalance: { + type: 'number', + description: 'average balance', + }, + }, + }, + customer: { + type: 'array', + items: [ + { + type: 'object', + properties: { + customerNumber: { + type: 'string', + description: 'The information of customers', + }, + firstName: { + type: 'string', + description: 'Fist Name of a customer', + }, + lastName: { + type: 'string', + description: 'Last Name of a customer', + }, + ssn: { + type: 'string', + description: 'SSN of a customer', + }, + customerSince: { + type: 'datetime', + description: 'Duration of a customer', + }, + street: { + type: 'string', + description: 'street of a customer', + }, + state: { + type: 'string', + description: 'state of a customer', + }, + city: { + type: 'string', + description: 'city of a customer', + }, + zip: { + type: 'string', + description: 'zip of a customer', + }, + lastUpdated: { + type: 'string', + description: 'lastUpdated date of address of customer', + }, + }, + }, + ], + }, + }, + }, + }, + }, + }, + '/account/create': { + post: { + 'x-operation-name': 'createAccount', + parameters: [ + { + name: 'accountInstance', + in: 'body', + description: 'The account instance.', + required: true, + type: 'object', + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + id: { + type: 'string', + description: 'The account id.', + }, + customerNumber: { + type: 'string', + description: 'The customer number.', + }, + balance: { + type: 'number', + description: 'The balance of the account.', + }, + branch: { + type: 'string', + description: 'The bank branch.', + }, + type: { + type: 'string', + description: 'The type of account ("savings" or "chequing").', + }, + minimumBalance: { + type: 'number', + description: 'The minimum balance for the account.', + }, + avgBalance: { + type: 'number', + description: 'The average balance of the account.', + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/example-microservices/services/facade/controllers/AccountManagementController.ts b/packages/example-microservices/services/facade/controllers/AccountManagementController.ts new file mode 100644 index 000000000000..21690fa51708 --- /dev/null +++ b/packages/example-microservices/services/facade/controllers/AccountManagementController.ts @@ -0,0 +1,46 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {api} from '@loopback/rest'; +import {def} from './AccountManagementController.api'; +import {AccountRepository} from '../repositories/account'; +import {CustomerRepository} from '../repositories/customer'; +import {TransactionRepository} from '../repositories/transaction'; +import bluebird = require('bluebird'); + +// tslint:disable:no-any + +@api(def) +export class AccountController { + accountRepository: AccountRepository; + customerRepository: CustomerRepository; + transactionRepository: TransactionRepository; + + constructor() { + this.accountRepository = new AccountRepository(); + this.customerRepository = new CustomerRepository(); + this.transactionRepository = new TransactionRepository(); + } + + async getSummary(accountNumber): Promise { + const account = await this.accountRepository.find(accountNumber); + const summary = await bluebird.props({ + account: account, + customer: this.customerRepository.find(account.customerNumber), + transaction: this.transactionRepository.find(accountNumber), + }); + return JSON.stringify(summary); + } + + async getAccount(accountNumber): Promise { + const account = await this.accountRepository.find(accountNumber); + return JSON.stringify(account); + } + + async createAccount(accountInstance): Promise { + const account = await this.accountRepository.create(accountInstance); + return JSON.stringify(account); + } +} diff --git a/packages/example-microservices/services/facade/index.ts b/packages/example-microservices/services/facade/index.ts new file mode 100644 index 000000000000..c01583395157 --- /dev/null +++ b/packages/example-microservices/services/facade/index.ts @@ -0,0 +1,42 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {AccountController} from './controllers/AccountManagementController'; + +class FacadeMicroservice extends Application { + private _startTime: Date; + + constructor() { + super(); + this.controller(AccountController); + } + + async start() { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return { + appName: 'facade', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }; + } +} + +async function main(): Promise { + const app = new FacadeMicroservice(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/facade/package.json b/packages/example-microservices/services/facade/package.json new file mode 100644 index 000000000000..9a2a4b642aa5 --- /dev/null +++ b/packages/example-microservices/services/facade/package.json @@ -0,0 +1,33 @@ +{ + "name": "@loopback-example-microservices/facade", + "version": "1.0.0", + "description": "The Facade microservice.", + "private": true, + "main": "index.ts", + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "dependencies": { + "@loopback/core": "^4.0.0-alpha.10", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "loopback-connector-swagger": "^3.2.0", + "loopback-datasource-juggler": "^3.4.1" + }, + "devDependencies": { + "@types/node": "^7.0.12" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "loopback-next", + "example", + "facade", + "microservice" + ], + "author": "IBM", + "license": "MIT" +} diff --git a/packages/example-microservices/services/facade/repositories/account/index.ts b/packages/example-microservices/services/facade/repositories/account/index.ts new file mode 100644 index 000000000000..c466f93974f7 --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/account/index.ts @@ -0,0 +1,34 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; + +// tslint:disable:no-any + +// mixin of data source into service is not yet available, swagger.json needs to +// be loaded synchronously (ie. can't instantiate in the class constructor) + +const ds = new DataSourceConstructor('AccountService', { + connector: 'swagger', + spec: 'repositories/account/swagger.json', +}); + +export class AccountRepository { + model; + + constructor() { + this.model = ds.createModel('AccountService', {}); + } + + async find(accountNumber) { + const response = await this.model.findById({id: accountNumber}); + const accounts = (response && response.obj) || []; + return accounts.length ? accounts[0] : {}; + } + + async create(accountInstance): Promise { + return await this.model.create(accountInstance); + } +} diff --git a/packages/example-microservices/services/facade/repositories/account/swagger.json b/packages/example-microservices/services/facade/repositories/account/swagger.json new file mode 100644 index 000000000000..f8fe3682d88e --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/account/swagger.json @@ -0,0 +1,69 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "account" + }, + "host": "localhost:3001", + "basePath": "/", + "paths": { + "/accounts": { + "get": { + "summary": "Find all instances of the model matched by filter from the data source.", + "operationId": "findById", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "Model id", + "required": true, + "type": "string", + "format": "JSON" + }, + { + "name": "filter", + "in": "query", + "description": "Filter defining fields and include - must be a JSON-encoded string ({\"something\":\"value\"})", + "required": false, + "type": "string", + "format": "JSON" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "type": "string" + } + } + }, + "deprecated": false + } + }, + "/accounts/create": { + "post": { + "summary": "Create an account instance.", + "operationId": "create", + "parameters": [ + { + "name": "accountInstance", + "in": "body", + "description": "The account instance.", + "required": true, + "type": "object", + "format": "JSON" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "type": "string" + } + } + }, + "deprecated": false + } + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/facade/repositories/customer/index.ts b/packages/example-microservices/services/facade/repositories/customer/index.ts new file mode 100644 index 000000000000..995423aa4c84 --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/customer/index.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; + +// mixin of data source into service is not yet available, swagger.json needs to +// be loaded synchronously (ie. can't instantiate in the class constructor) + +const SwaggerClient = require('swagger-client'); +const ds = new DataSourceConstructor('CustomerService', { + connector: 'swagger', + spec: 'repositories/customer/swagger.json', +}); + +export class CustomerRepository { + model; + + constructor() { + this.model = ds.createModel('CustomerService', {}); + } + + async find(customerNumber) { + const response = await this.model.findById({id: customerNumber}); + return (response && response.obj) || []; + } +} diff --git a/packages/example-microservices/services/facade/repositories/customer/swagger.json b/packages/example-microservices/services/facade/repositories/customer/swagger.json new file mode 100644 index 000000000000..0306176eb0c9 --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/customer/swagger.json @@ -0,0 +1,44 @@ +{ + "swagger":"2.0", + "info":{ + "version":"1.0.0", + "title":"customer" + }, + "host": "localhost:3002", + "basePath":"/", + "paths":{ + "/customers":{ + "get":{ + "summary":"Find all instances of the model matched by filter from the data source.", + "operationId":"findById", + "parameters":[ + { + "name":"id", + "in":"query", + "description":"Model id", + "required":true, + "type":"string", + "format":"JSON" + }, + { + "name":"filter", + "in":"query", + "description":"Filter defining fields and include - must be a JSON-encoded string ({\"something\":\"value\"})", + "required":false, + "type":"string", + "format":"JSON" + } + ], + "responses":{ + "200":{ + "description":"Request was successful", + "schema":{ + "type":"string" + } + } + }, + "deprecated":false + } + } + } +} diff --git a/packages/example-microservices/services/facade/repositories/transaction/index.ts b/packages/example-microservices/services/facade/repositories/transaction/index.ts new file mode 100644 index 000000000000..a3f3ab617c00 --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/transaction/index.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; + +// mixin of data source into service is not yet available, swagger.json needs to +// be loaded synchronously (ie. can't instantiate in the class constructor) + +const SwaggerClient = require('swagger-client'); +const ds = new DataSourceConstructor('TransactionService', { + connector: 'swagger', + spec: 'repositories/transaction/swagger.json', +}); + +export class TransactionRepository { + model; + + constructor() { + this.model = ds.createModel('TransactionService', {}); + } + + async find(accountNumber) { + const response = await this.model.findById({id: accountNumber}); + return (response && response.obj) || []; + } +} diff --git a/packages/example-microservices/services/facade/repositories/transaction/swagger.json b/packages/example-microservices/services/facade/repositories/transaction/swagger.json new file mode 100644 index 000000000000..170cc482a2ad --- /dev/null +++ b/packages/example-microservices/services/facade/repositories/transaction/swagger.json @@ -0,0 +1,44 @@ +{ + "swagger":"2.0", + "info":{ + "version":"1.0.0", + "title":"transaction" + }, + "host": "localhost:3003", + "basePath":"/", + "paths":{ + "/transactions":{ + "get":{ + "summary":"Find all instances of the model matched by filter from the data source.", + "operationId":"findById", + "parameters":[ + { + "name":"id", + "in":"query", + "description":"Model id", + "required":true, + "type":"string", + "format":"JSON" + }, + { + "name":"filter", + "in":"query", + "description":"Filter defining fields and include - must be a JSON-encoded string ({\"something\":\"value\"})", + "required":false, + "type":"string", + "format":"JSON" + } + ], + "responses":{ + "200":{ + "description":"Request was successful", + "schema":{ + "type":"string" + } + } + }, + "deprecated":false + } + } + } +} diff --git a/packages/example-microservices/services/facade/tsconfig.json b/packages/example-microservices/services/facade/tsconfig.json new file mode 100644 index 000000000000..429a600c5bb4 --- /dev/null +++ b/packages/example-microservices/services/facade/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "node_modules/loopback-next/packages/*" + ] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": [ + "node_modules/@types" + ], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/example-microservices/services/todo-legacy/README.md b/packages/example-microservices/services/todo-legacy/README.md new file mode 100644 index 000000000000..8769ee14d6d9 --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/README.md @@ -0,0 +1,175 @@ +# Todo Application with Legacy Juggler + +## Summary +A REST API for managing Todo entries. + +## Installation +This example requires Node.js 8.x or higher. +Install dependencies: +``` +npm i +``` + +After you've installed your dependencies, you'll need to configure the +`datasource.ts` file to point towards your target database. + +By default, this example is configured to use the loopback-connector-mysql +module, with credentials defined by its `setup.sh` file (which we use for +unit tests). + +Feel free to install a different connector, and change `datasource.ts` +accordingly! + +## Overview + +This sample application demonstrates a simple CRUD API for a Todo list. +This application does not currently have an explorer component. + +Perform a series of CRUD operations using GET, POST, PUT, PATCH and DELETE +on the `localhost:/todo` route! + +Currently, only the title filter is supported: +(ex. `GET localhost:3000/todo?title=foo`) + +In this example app, the TodoRepository is using the injected datasource +configuration (which uses the loopback@3.x format) in combination with the +DataSourceConstructor provided by `legacy-juggler-bridge.ts/js`. +Additionally, it injects a loopback@3.x model definition to construct the +Todo model class at runtime. + +## Use + +Run the app: +``` +npm start +``` + +By default, it will be on port 3000, but you can specify the PORT environment +variable to change this: +``` +# There, now it's different! +PORT=3001 npm start +``` + +Use `curl` or your favourite REST client to access the endpoints! + +### Create a Todo + +`POST /todo` + +Body: +```json +{ + "title": "Make GUI", + "body": "So that I don't have to keep using curl to make my todo entries..." +} +``` + +Returns: +```json +{ + "id": 1, + "title": "Make GUI", + "body": "So that I don't have to keep using curl to make my todo entries..." +} +``` + +### Get Todo by ID + +`GET /todo/1` + +Returns: +```json +{ + "id": 1, + "title": "Make GUI", + "body": "So that I don't have to keep using curl to make my todo entries..." +} +``` + +### Find Todo by Title + +`GET /todo?title=Make%20%GUI` + +Returns: + +```json +[ + { + "id": 1, + "title": "Make GUI", + "body": "So that I don't have to keep using curl to make my todo entries..." + }, + { + "id": 2, + "title": "Make GUI", + "body": "Wait, I think I already made this todo... :S" + }, +] +``` + +### Replace Todo +`PUT /todo/2` +Body: +```json +{ + "title": "Make GUI Shiny", + "body": "Yeah, definitely a shiny GUI!" +} +``` + +Returns: +```json + { + "id": 2, + "title": "Make Shiny GUI", + "body": "Yeah, definitely a shiny GUI!" + }, +``` + +### Update Todo + +`PATCH /todo/2` +Body: +```json +{ + "title": "Make simple GUI" +} +``` + +Returns: +``` + { + "id": 2, + "title": "Make simple GUI", + "body": "Wait, I think I already made this todo... :S" + }, +``` + +### Delete a Todo + +`DELETE /todo/{id}` + +Returns: +```json +{ + "count": 1 +} +``` + +### Delete Todo by Title +`DELETE /todo?title=Make%GUI` + +Returns: +```json +{ + "count": 3 +} +``` + +## Tests +Run tests with `npm test`! + +## What's Next? +Now that you've got a working example to play with, feel free to +begin hacking on it! diff --git a/packages/example-microservices/services/todo-legacy/application.ts b/packages/example-microservices/services/todo-legacy/application.ts new file mode 100644 index 000000000000..4dd1a1a9d707 --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/application.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {TodoController} from './controllers/todo-controller'; +import { + juggler, + DataSourceConstructor, + DefaultCrudRepository, +} from '@loopback/repository'; +import {datasources} from './datasources'; + +export class TodoApplication extends Application { + private _startTime: Date; + + constructor() { + super(); + const app = this; + const ds = datasources['ds']; + // Controller bindings + app.controller(TodoController); + + const datasource = new DataSourceConstructor('ds', ds); + app.bind('datasources.ds').to(datasource); + + // Server protocol bindings + app.bind('servers.http.enabled').to(true); + app.bind('servers.https.enabled').to(true); + } + + async start(): Promise { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return JSON.stringify( + { + appName: 'todo-legecy', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }, + null, + 2, + ); + } +} diff --git a/packages/example-microservices/services/todo-legacy/controllers/todo-controller.api.ts b/packages/example-microservices/services/todo-legacy/controllers/todo-controller.api.ts new file mode 100644 index 000000000000..b90f71518cc0 --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/controllers/todo-controller.api.ts @@ -0,0 +1,191 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + definitions: { + todo: { + properties: { + id: { + type: 'number', + description: 'The ID number of the Todo entry.', + }, + title: { + type: 'string', + description: 'The title of the Todo entry.', + }, + body: { + type: 'string', + description: 'The body of the Todo entry.', + }, + }, + }, + }, + paths: { + '/todo': { + get: { + 'x-operation-name': 'get', + parameters: [ + { + name: 'title', + in: 'query', + description: 'The title of the todo entry.', + required: false, + type: 'string', + format: 'JSON', + }, + { + name: 'filter', + in: 'query', + description: 'The title of the todo entry.', + required: false, + type: 'object', + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + $ref: '#/definitions/todo', + }, + }, + }, + }, + post: { + 'x-operation-name': 'create', + parameters: [ + { + name: 'data', + in: 'body', + description: 'The Todo model instance.', + required: true, + format: 'JSON', + }, + ], + responses: { + 201: { + schema: { + $ref: '#/definitions/todo', + }, + }, + }, + }, + delete: { + 'x-operation-name': 'delete', + parameters: [ + { + name: 'title', + in: 'query', + description: + 'The criteria used to narrow down the number of customers returned.', + required: false, + type: 'string', + format: 'JSON', + }, + ], + responses: { + 204: { + description: 'The resource has been deleted.', + }, + }, + }, + }, + '/todo/{id}': { + get: { + 'x-operation-name': 'getById', + parameters: [ + { + name: 'id', + in: 'path', + description: + 'The criteria used to narrow down the number of customers returned.', + required: false, + type: 'string', + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + $ref: '#/definitions/todo', + }, + }, + }, + }, + put: { + 'x-operation-name': 'replace', + parameters: [ + { + name: 'id', + in: 'path', + description: 'The todo ID.', + required: true, + type: 'string', + format: 'JSON', + }, + { + name: 'data', + in: 'body', + description: 'The Todo model instance.', + required: true, + format: 'JSON', + }, + ], + responses: { + 201: { + schema: { + $ref: '#/definitions/todo', + }, + }, + }, + }, + patch: { + 'x-operation-name': 'update', + parameters: [ + { + name: 'id', + in: 'path', + description: 'The todo ID.', + required: true, + type: 'string', + format: 'JSON', + }, + { + name: 'data', + in: 'body', + description: 'The Todo model instance.', + required: true, + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + $ref: '#/definitions/todo', + }, + }, + }, + }, + delete: { + 'x-operation-name': 'deleteById', + parameters: [ + { + name: 'id', + in: 'path', + description: 'The todo ID.', + required: false, + type: 'string', + format: 'JSON', + }, + ], + responses: { + 204: { + description: 'The resource has been deleted.', + }, + }, + }, + }, + }, +}; diff --git a/packages/example-microservices/services/todo-legacy/controllers/todo-controller.ts b/packages/example-microservices/services/todo-legacy/controllers/todo-controller.ts new file mode 100644 index 000000000000..7bedc8821d1f --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/controllers/todo-controller.ts @@ -0,0 +1,83 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, inject} from '@loopback/core'; +import {api} from '@loopback/rest'; +import {def} from './todo-controller.api'; +import {Todo} from '../models/todo'; +import * as util from 'util'; +import {EntityCrudRepository, repository} from '@loopback/repository'; + +@api(def) +export class TodoController { + @repository(Todo, 'ds') + repository: EntityCrudRepository; + + constructor() {} + + async get(title?: string): Promise { + const filter = title ? {where: {title: title}} : {}; + return await this.repository.find(filter); + } + + async getById(id: number): Promise { + return await this.repository.find({where: {id: id}}); + } + + async create(body: Object) { + return await this.repository.create(new Todo(body)); + } + + async update(id: number, body: Object): Promise { + let success: boolean; + if (id) { + const todo = new Todo(body); + todo.id = id; + success = await this.repository.updateById(id, todo); + // FIXME(kev): Unhandled error is thrown if you attempt to return + // the boolean value that the repository.update method returns. + return Promise.resolve({count: success ? 1 : 0}); + } else if (body) { + const result = await this.repository.updateAll(new Todo(body)); + return Promise.resolve({count: result}); + } else { + return Promise.reject( + new Error('Cowardly refusing to update all todos!'), + ); + } + } + + async replace(id: number, body: Todo): Promise { + const success = await this.repository.replaceById(id, new Todo(body)); + // FIXME(kev): Unhandled error is thrown if you attempt to return + // the boolean value that the repository.replaceById method returns. + return Promise.resolve({count: success ? 1 : 0}); + } + + async delete(title: string): Promise { + if (!title) { + return Promise.reject(new Error('You must provide a filter query!')); + } + const filter = {where: {title: title}}; + const result = await this.repository.deleteAll(filter); + return Promise.resolve({count: result}); + } + + async deleteById(id: number): Promise { + const success = await this.repository.deleteById(id); + // FIXME(kev): Unhandled error is thrown if you attempt to return + // the boolean value that the repository.replaceById method returns. + return Promise.resolve({count: success ? 1 : 0}); + } +} +/** + * Helper class to define the return type of operations that return + * affected item counts. + * + * @class AffectedItems + */ +class AffectedItems { + constructor(public count: number) {} +} diff --git a/packages/example-microservices/services/todo-legacy/datasources.ts b/packages/example-microservices/services/todo-legacy/datasources.ts new file mode 100644 index 000000000000..fb8aa103f43f --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/datasources.ts @@ -0,0 +1,64 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// tslint:disable:no-any + +export const datasources: DataSourceConfig = { + ds: { + name: 'ds', + connector: 'mysql', + host: 'localhost', + port: 3306, + database: 'testdb', + password: 'pass', + user: 'root', + }, +}; + +export interface DataSourceConfig { + [datasource: string]: DataSourceDefinition; +} + +/** + * The parameters required to define a MySQL datasource. + * + * @export + * @interface DataSourceDefinition + */ +export interface DataSourceDefinition { + /** + * The name of the connection (for programmatic reference). + * + * @type {string} + * @memberof DataSourceDefinition + */ + name: string; + /** + * The identifying name of the legacy connector module used to interact with + * the datasource. + * (ex. "mysql", "mongodb", "db2", etc...) + * + * @type {string} + * @memberof DataSourceDefinition + */ + connector: string; + /** + * + * The accessible machine name, domain address or IP address of the + * datasource. + * @type {string} + * @memberof DataSourceDefinition + */ + host: string; + /** + * The port number on which the datasource is listening for connections. + * + * @type {number} + * @memberof DataSourceDefinition + */ + port: number; + // Allow arbitrary extension of object. + [extras: string]: any; +} diff --git a/packages/example-microservices/services/todo-legacy/index.ts b/packages/example-microservices/services/todo-legacy/index.ts new file mode 100644 index 000000000000..4e903e87b54e --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/index.ts @@ -0,0 +1,18 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {TodoApplication} from './application'; + +async function main(): Promise { + const app = new TodoApplication(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/todo-legacy/models/todo.ts b/packages/example-microservices/services/todo-legacy/models/todo.ts new file mode 100644 index 000000000000..5000396fd2e0 --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/models/todo.ts @@ -0,0 +1,39 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class Todo extends Entity { + @property({ + type: 'number', + id: true, + description: 'The ID number of the Todo entry.', + }) + id: number; + + @property({ + type: 'string', + description: 'The title of the todo.', + }) + title: string; + + @property({ + type: 'string', + description: 'The main body of the todo.', + }) + body: string; + + constructor(body?: Object) { + super(); + if (body) { + Object.assign(this, body); + } + } + + getId() { + return this.id; + } +} diff --git a/packages/example-microservices/services/todo-legacy/package.json b/packages/example-microservices/services/todo-legacy/package.json new file mode 100644 index 000000000000..d6f37d95bb1c --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/package.json @@ -0,0 +1,35 @@ +{ + "name": "@loopback-example-microservices/todo", + "version": "1.0.0", + "description": "Example Todo list application", + "private": true, + "main": "index", + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "mocha --compilers ts:ts-node/register,tsx:ts-node/register test/**/*.test.ts" + }, + "keywords": [], + "author": "", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "devDependencies": { + "@loopback/testlab": "^4.0.0-alpha.6", + "@types/mocha": "^2.2.43", + "mocha": "^3.5.3" + }, + "dependencies": { + "@loopback/core": "^4.0.0-alpha.13", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "@types/lodash": "^4.14.67", + "loopback-connector-mysql": "^4.2.0", + "ts-node": "^3.0.4", + "typescript": "^2.4.1" + } +} diff --git a/packages/example-microservices/services/todo-legacy/test/controller/todo-controller.test.ts b/packages/example-microservices/services/todo-legacy/test/controller/todo-controller.test.ts new file mode 100644 index 000000000000..b5753cd60cec --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/test/controller/todo-controller.test.ts @@ -0,0 +1,146 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import 'mocha'; +import * as _ from 'lodash'; +import {expect, sinon} from '@loopback/testlab'; +import { + DefaultCrudRepository, + DataSourceConstructor, + ModelBaseConstructor, +} from '@loopback/repository'; +import {TodoController} from '../../controllers/todo-controller'; +import {Todo} from '../../models/todo'; + +import * as util from 'util'; + +describe('TodoController', () => { + const controller = new TodoController(); + // NOTE: Creating the datasource and model definition with + // the real functions, and then stubbing them is easier than + // building the stubs and fakes by hand! + const datasource = new DataSourceConstructor({ + name: 'ds', + connector: 'memory', + }); + const repository = new DefaultCrudRepository(Todo, datasource); + controller.repository = repository; + + describe('getTodo', () => { + const sandbox = sinon.sandbox.create(); + beforeEach(() => { + sandbox.restore(); + }); + it('returns all todos when called without ID', async () => { + const stub = sandbox.stub(repository, 'find'); + const result = await controller.get(); + expect.ok(stub.called, 'find was called'); + expect.deepEqual(stub.getCall(0).args, [{}], 'args were correct'); + }); + it('returns the correct todo by ID', async () => { + const stub = sandbox.stub(repository, 'find'); + const result = await controller.getById(1); + expect.ok(stub.called, 'find was called'); + }); + it('can filter by title', async () => { + const stub = sandbox.stub(repository, 'find'); + const result = await controller.get('test2'); + expect.ok(stub.called, 'find was called'); + expect.deepEqual( + stub.getCall(0).args, + [ + { + where: {title: 'test2'}, + }, + ], + 'controller created correct filter object', + ); + }); + }); + + describe('createTodo', () => { + const sandbox = sinon.sandbox.create(); + beforeEach(() => { + sandbox.restore(); + }); + it('calls create on the repository', async () => { + const stub = sandbox.stub(repository, 'create'); + const result = await controller.create({ + title: 'foo', + body: 'bar', + }); + expect.ok(stub.called, 'create was called'); + }); + }); + + describe('replaceTodo', () => { + const sandbox = sinon.sandbox.create(); + beforeEach(() => { + sandbox.restore(); + }); + it('returns an affected item count of 1 on success', async () => { + const stub = sandbox.stub(controller.repository, 'replaceById'); + const replacement = new Todo(); + Object.assign(replacement, { + id: 1, + title: 'foo', + body: 'bar', + }); + const result = await controller.replace(1, replacement); + expect.ok(stub.called, 'replace was called'); + }); + }); + + describe('updateTodo', () => { + const sandbox = sinon.sandbox.create(); + beforeEach(() => { + sandbox.restore(); + }); + it('returns the updated version of the object', async () => { + const stub = sandbox.stub(controller.repository, 'updateById'); + const replacement = { + id: 1, + title: 'foo', + }; + const expected = _.merge({id: 1}, replacement); + const result = await controller.update(1, replacement); + expect.ok(stub.called, 'update was called'); + }); + // There's no unhappy path tests here for missing ID, because + // it's handled at the repository layer, which we are stubbing! + }); + + describe('deleteTodo', () => { + const sandbox = sinon.sandbox.create(); + beforeEach(() => { + sandbox.restore(); + }); + it('works on one item', async () => { + const stub = sandbox.stub(controller.repository, 'deleteById'); + const result = await controller.deleteById(1); + expect.ok(stub.called, 'delete was called'); + // The null filter is automatically replaced with an empty object in + // controller layer! + expect.deepEqual(stub.getCall(0).args, [1], 'args were correct'); + }); + + it('can filter by title', async () => { + const stub = sandbox.stub(controller.repository, 'deleteAll'); + const result = await controller.delete('test2'); + expect.ok(stub.called, 'result exists'); + expect.deepEqual( + stub.getCall(0).args, + [ + { + where: { + title: 'test2', + }, + }, + ], + 'controller created correct filter object', + ); + }); + }); +}); diff --git a/packages/example-microservices/services/todo-legacy/tsconfig.json b/packages/example-microservices/services/todo-legacy/tsconfig.json new file mode 100644 index 000000000000..6d1b5e1a9e1d --- /dev/null +++ b/packages/example-microservices/services/todo-legacy/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2017", + "sourceMap": true, + "outDir": "lib", + "experimentalDecorators": true, + "strict": true + }, + "compileOnSave": true +} diff --git a/packages/example-microservices/services/transaction/bin/get-transactions b/packages/example-microservices/services/transaction/bin/get-transactions new file mode 100755 index 000000000000..e2f1e666493c --- /dev/null +++ b/packages/example-microservices/services/transaction/bin/get-transactions @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +curl -s localhost:3003/transactions | jq \ No newline at end of file diff --git a/packages/example-microservices/services/transaction/controllers/TransactionController.api.ts b/packages/example-microservices/services/transaction/controllers/TransactionController.api.ts new file mode 100644 index 000000000000..3e560c898fae --- /dev/null +++ b/packages/example-microservices/services/transaction/controllers/TransactionController.api.ts @@ -0,0 +1,61 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export const def = { + basePath: '/', + paths: { + '/transactions': { + get: { + 'x-operation-name': 'getTransactions', + parameters: [ + { + name: 'id', + in: 'query', + description: 'The transaction id', + required: true, + type: 'string', + format: 'JSON', + }, + { + name: 'filter', + in: 'query', + description: + 'The criteria used to narrow down the number of transactions returned.', + required: false, + type: 'string', + format: 'JSON', + }, + ], + responses: { + 200: { + schema: { + TransactionId: { + type: 'string', + description: 'The unique identifier for the transaction.', + }, + dateTime: { + type: 'date', + description: 'The date and time of the transaction.', + }, + accountNo: { + type: 'string', + description: 'The associated account number.', + }, + amount: { + type: 'number', + description: 'The amount being consider in the transaction.', + }, + transactionType: { + type: 'string', + description: + 'The type of transaction. Can be "credit" or "debit".', + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/example-microservices/services/transaction/controllers/TransactionController.ts b/packages/example-microservices/services/transaction/controllers/TransactionController.ts new file mode 100644 index 000000000000..47eb8273f16e --- /dev/null +++ b/packages/example-microservices/services/transaction/controllers/TransactionController.ts @@ -0,0 +1,28 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {api} from '@loopback/rest'; +import {def} from './TransactionController.api'; +import {TransactionRepository} from '../repositories/transaction'; + +// tslint:disable:no-any + +@api(def) +export class TransactionController { + repository: TransactionRepository; + + constructor() { + this.repository = new TransactionRepository(); + } + + async getTransactions(filter): Promise { + const transactions = await this.repository.find(filter); + const response = []; + transactions.forEach(transaction => { + response.push(transaction.toJSON()); + }); + return response; + } +} diff --git a/packages/example-microservices/services/transaction/index.ts b/packages/example-microservices/services/transaction/index.ts new file mode 100644 index 000000000000..568ce21c0cce --- /dev/null +++ b/packages/example-microservices/services/transaction/index.ts @@ -0,0 +1,44 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application} from '@loopback/core'; +import {TransactionController} from './controllers/TransactionController'; + +class TransactionApplication extends Application { + private _startTime: Date; + + constructor() { + super(); + const app = this; + app.controller(TransactionController); + app.bind('http.port').to(3003); + } + + async start() { + this._startTime = new Date(); + return super.start(); + } + + async info() { + const port: Number = await this.get('http.port'); + + return { + appName: 'transaction', + uptime: Date.now() - this._startTime.getTime(), + url: 'http://127.0.0.1:' + port, + }; + } +} + +async function main(): Promise { + const app = new TransactionApplication(); + await app.start(); + console.log('Application Info:', await app.info()); +} + +main().catch(err => { + console.log('Cannot start the app.', err); + process.exit(1); +}); diff --git a/packages/example-microservices/services/transaction/package.json b/packages/example-microservices/services/transaction/package.json new file mode 100644 index 000000000000..804c3085edd6 --- /dev/null +++ b/packages/example-microservices/services/transaction/package.json @@ -0,0 +1,32 @@ +{ + "name": "@loopback-example-microservices/transaction", + "version": "1.0.0", + "description": "The Transaction microservice.", + "private": true, + "main": "index.ts", + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "dependencies": { + "@loopback/core": "^4.0.0-alpha.10", + "@loopback/repository": "^4.0.0-alpha.5", + "@loopback/rest": "^4.0.0-alpha.22", + "loopback-datasource-juggler": "^3.4.1" + }, + "devDependencies": { + "@types/node": "^7.0.12" + }, + "scripts": { + "start": "ts-node index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "loopback-next", + "example", + "transaction", + "microservice" + ], + "author": "IBM", + "license": "MIT" +} diff --git a/packages/example-microservices/services/transaction/repositories/transaction/datasources/local-fs/data.json b/packages/example-microservices/services/transaction/repositories/transaction/datasources/local-fs/data.json new file mode 100644 index 000000000000..de9bffb20f53 --- /dev/null +++ b/packages/example-microservices/services/transaction/repositories/transaction/datasources/local-fs/data.json @@ -0,0 +1,20 @@ +{ + "ids": { + "Transaction": 13 + }, + "models": { + "Transaction": { + "DEBIT0001": "{\"TransactionId\":\"DEBIT0001\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":20,\"transactionType\":\"debit\"}", + "DEBIT0002": "{\"TransactionId\":\"DEBIT0002\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":20,\"transactionType\":\"debit\"}", + "DEBIT0003": "{\"TransactionId\":\"DEBIT0003\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":40,\"transactionType\":\"credit\"}", + "DEBIT0004": "{\"TransactionId\":\"DEBIT0004\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":50,\"transactionType\":\"credit\"}", + "DEBIT0005": "{\"TransactionId\":\"DEBIT0005\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321122\",\"amount\":50,\"transactionType\":\"credit\"}", + "DEBIT0007": "{\"TransactionId\":\"DEBIT0007\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK54520000\",\"amount\":100,\"transactionType\":\"credit\"}", + "DEBIT0009": "{\"TransactionId\":\"DEBIT0009\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321199\",\"amount\":140,\"transactionType\":\"credit\"}", + "DEBIT0010": "{\"TransactionId\":\"DEBIT0010\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK52321199\",\"amount\":20,\"transactionType\":\"debit\"}", + "DEBIT0011": "{\"TransactionId\":\"DEBIT0011\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":300,\"transactionType\":\"credit\"}", + "DEBIT0012": "{\"TransactionId\":\"DEBIT0012\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":100,\"transactionType\":\"debit\"}", + "DEBIT0013": "{\"TransactionId\":\"DEBIT0013\",\"dateTime\":\"2017-03-11T00:27:52.422Z\",\"accountNo\":\"CHK99999999\",\"amount\":50,\"transactionType\":\"debit\"}" + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/transaction/repositories/transaction/index.ts b/packages/example-microservices/services/transaction/repositories/transaction/index.ts new file mode 100644 index 000000000000..05d2ab02d69a --- /dev/null +++ b/packages/example-microservices/services/transaction/repositories/transaction/index.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-microservices +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {juggler, DataSourceConstructor} from '@loopback/repository'; +const modelDefinition = require('./models/transaction/model-definition.json'); + +// tslint:disable:no-any + +export class TransactionRepository { + model; + + constructor() { + const ds = new DataSourceConstructor('local-fs', { + connector: 'memory', + file: './repositories/transaction/datasources/local-fs/data.json', + }); + this.model = ds.createModel('Transaction', modelDefinition); + } + + async find(id): Promise { + return await this.model.find({where: {accountNo: id}}); + } +} diff --git a/packages/example-microservices/services/transaction/repositories/transaction/models/transaction/model-definition.json b/packages/example-microservices/services/transaction/repositories/transaction/models/transaction/model-definition.json new file mode 100644 index 000000000000..0d1116d50a45 --- /dev/null +++ b/packages/example-microservices/services/transaction/repositories/transaction/models/transaction/model-definition.json @@ -0,0 +1,26 @@ +{ + "name": "Transaction", + "properties": { + "TransactionId": { + "type": "string", + "required": true, + "id": true + }, + "dateTime": { + "type": "date", + "required": true + }, + "accountNo": { + "type": "string", + "required": true + }, + "amount": { + "type": "number", + "required": true + }, + "transactionType": { + "type": "string", + "required": true + } + } +} \ No newline at end of file diff --git a/packages/example-microservices/services/transaction/tsconfig.json b/packages/example-microservices/services/transaction/tsconfig.json new file mode 100644 index 000000000000..741c22be99a3 --- /dev/null +++ b/packages/example-microservices/services/transaction/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es6", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": [ + "node_modules/@types" + ], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/example-microservices/tsconfig.json b/packages/example-microservices/tsconfig.json new file mode 100644 index 000000000000..20496bb27106 --- /dev/null +++ b/packages/example-microservices/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["node_modules/loopback-next/packages/*"] + }, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es6", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "typeRoots": ["node_modules/@types"], + "inlineSources": true + }, + "include": [ + "**/*" + ], + "exclude": [ + "**/*.d.ts" + ] +} diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index 5fe1fa97fbf0..9014ce36e730 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -158,7 +158,7 @@ export abstract class Model { /** * Convert to a plain object as DTO */ - toObject(options?: Options): Object { + toObject(options?: Options): AnyObject { let obj: AnyObject; if (options && options.ignoreUnknownProperties === false) { obj = {};