From e236423018e8dfaefcfddb429c332c8f69ed7bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 19 Jun 2018 13:03:40 +0200 Subject: [PATCH] docs: add geo-coding service to Todo tutorial --- docs/site/sidebars/lb4_sidebar.yml | 4 + docs/site/todo-tutorial-geocoding-service.md | 255 ++++++++++++++++++ .../site/todo-tutorial-putting-it-together.md | 10 + examples/todo/README.md | 2 + examples/todo/src/application.ts | 2 + .../src/datasources/geocoder.datasource.ts | 4 +- examples/todo/src/datasources/index.ts | 1 + 7 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 docs/site/todo-tutorial-geocoding-service.md diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index 590a74a9fff6..2566d435840e 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -53,6 +53,10 @@ children: url: todo-tutorial-putting-it-together.html output: 'web, pdf' + - title: 'Bonus: Integrate with a geo-coding service' + url: todo-tutorial-geocoding-service.html + output: 'web, pdf' + - title: 'Key concepts' url: Concepts.html output: 'web, pdf' diff --git a/docs/site/todo-tutorial-geocoding-service.md b/docs/site/todo-tutorial-geocoding-service.md new file mode 100644 index 000000000000..be133598974e --- /dev/null +++ b/docs/site/todo-tutorial-geocoding-service.md @@ -0,0 +1,255 @@ +--- +lang: en +title: 'Integrate with a geo-coding service' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/todo-tutorial-geocoding-service.html +summary: LoopBack 4 Todo Application Tutorial - Integrate with a geo-coding service +--- + +### Services + +To call other APIs and web services from LoopBack applications, we recommend to +use Service Proxies as a design pattern for encapsulating low-level +implementation details of communication with 3rd-party services and providing +JavaScript/TypeScript API that's easy to consume e.g. from Controllers. See +[Calling other APIs and web services](./Calling-other-APIs-and-web-services.md) +for more details. + +In LoopBack, each service proxy is backed by a +[DataSource](./todo-tutorial-datasource.md), this datasource leverages one of +the service connectors to make outgoing requests and parse responses returned by +the service. + +In our tutorial, we will leverage +[US Census Geocoder API](https://geocoding.geo.census.gov/geocoder/) to convert +textual US addresses into GPS coordinates, thus enabling client applications of +our Todo API to display location-based reminders, + +{% include tip.html content=" +In a real project, you may want to use a geocoding service that covers more +countries beyond USA and provides faster responses than US Census Geocoder API, +for example IBM's [Weather Company Data](https://console.bluemix.net/catalog/services/weather-company-data) +or [Google Maps Platform](https://developers.google.com/maps/documentation/geocoding). +" %} + +### Configure the backing datasource + +Run `lb4 datasource` to define a new datasource connecting to Geocoder REST +service. When prompted for a connector to use, select "REST services". + +``` +$ lb4 datasource +? Datasource name: geocoder +? Select the connector for geocoder: REST services (supported by StrongLoop) +? Base URL for the REST service: +? Default options for the request: +? An array of operation templates: +? Use default CRUD mapping: No + create src/datasources/geocoder.datasource.json + create src/datasources/geocoder.datasource.ts + # npm will install dependencies now + update src/datasources/index.ts + +Datasource geocoder was created in src/datasources/ +``` + +Edit the newly created datasource configuration to configure Geocoder API +endpoints. Configuration options provided by REST Connector are described in our +docs here: [REST connector](/doc/en/lb3/REST-connector.html). + +#### /src/datasources/geocoder.datasource.json + +```json +{ + "connector": "rest", + "options": { + "headers": { + "accept": "application/json", + "content-type": "application/json" + } + }, + "operations": [ + { + "template": { + "method": "GET", + "url": + "https://geocoding.geo.census.gov/geocoder/locations/onelineaddress", + "query": { + "format": "{format=json}", + "benchmark": "Public_AR_Current", + "address": "{address}" + }, + "responsePath": "$.result.addressMatches[*].coordinates" + }, + "functions": { + "geocode": ["address"] + } + } + ] +} +``` + +### Implement a service provider + +Create a new directory `src/services` and add the following two new files: + +- `src/geocoder.service.ts` defining TypeScript interfaces for Geocoder service + and implementing a service proxy provider. +- `src/index.ts` providing a conventient access to all services via a single + `import` statement. + +#### src/geocoder.service.ts + +```ts +import {getService, juggler} from '@loopback/service-proxy'; +import {inject, Provider} from '@loopback/core'; +import {GeocoderDataSource} from '../datasources/geocoder.datasource'; + +export interface GeoPoint { + /** + * latitude + */ + y: number; + + /** + * longitude + */ + x: number; +} + +export interface GeocoderService { + geocode(address: string): Promise; +} + +export class GeocoderServiceProvider implements Provider { + constructor( + @inject('datasources.geocoder') + protected datasource: juggler.DataSource = new GeocoderDataSource(), + ) {} + + value(): GeocoderService { + return getService(this.datasource); + } +} +``` + +#### src/services/index.ts + +```ts +export * from './geocoder.service'; +``` + +### Register the service for dependency injection + +Because `@loopback/boot` does not support loading of services yet (see +[issue #1439](https://github.com/strongloop/loopback-next/issues/1439)), we need +to add few code snippets to our Application class to take care of this task. + +#### src/application.ts + +```ts +export class TodoListApplication extends BootMixin( + RepositoryMixin(RestApplication), +) { + constructor(options?: ApplicationConfig) { + super(options); + // etc., keep the existing code without changes + + // ADD THE FOLLOWING LINE AT THE END + this.setupServices(); + } + + // ADD THE FOLLOWING TWO METHODS + setupServices() { + this.service(GeocoderServiceProvider); + } + + service(provider: Constructor>) { + const key = `services.${provider.name.replace(/Provider$/, '')}`; + this.bind(key).toProvider(provider); + } +} +``` + +### Enhance Todo model with location data + +Add two new properties to our Todo model: `remindAtAddress` and `remindAtGeo`. + +#### src/models/todo.model.ts + +```ts +@model() +export class Todo extends Entity { + // original code remains unchanged, add the following two properties: + + @property({ + type: 'string', + }) + remindAtAddress: string; // address,city,zipcode + + @property({ + type: 'string', + }) + remindAtGeo: string; // latitude,longitude +} +``` + +### Look up address location in the controller + +Finally, modify `TodoController` to look up the address and convert it to GPS +coordinates when a new Todo item is created. + +Modify the Controller constructor to receive `GeocoderService` as a new +dependency. + +#### src/controllers/todo.controller.ts + +```ts +export class TodoController { + constructor( + @repository(TodoRepository) protected todoRepo: TodoRepository, + @inject('services.GeocoderService') protected geoService: GeocoderService, + ) {} + + // etc. +} +``` + +Modify `createTodo` method to look up the address provided in `remindAtAddress` +property and convert it to GPS coordinates stored in `remindAtGeo`. + +#### src/controllers/todo.controller.ts + +```ts +export class TodoController { + // constructor, etc. + + @post('/todos') + async createTodo(@requestBody() todo: Todo) { + if (!todo.title) { + throw new HttpErrors.BadRequest('title is required'); + } + + if (todo.remindAtAddress) { + // TODO handle "address not found" + const geo = await this.geoService.geocode(todo.remindAtAddress); + // Encode the coordinates as "lat,lng" + todo.remindAtGeo = `${geo[0].y},${geo[0].x}`; + } + + return await this.todoRepo.create(todo); + } + + // other endpoints remain unchanged +} +``` + +Congratulations! Now your Todo API makes it easy to enter an address for a +reminder and have the client application show the reminder when the device +reaches close proximity of that address based on GPS location. + +### Navigation + +Previous step: [Putting it all together](todo-tutorial-putting-it-together.md) diff --git a/docs/site/todo-tutorial-putting-it-together.md b/docs/site/todo-tutorial-putting-it-together.md index 2125500cb091..7fb06f8b81fc 100644 --- a/docs/site/todo-tutorial-putting-it-together.md +++ b/docs/site/todo-tutorial-putting-it-together.md @@ -118,6 +118,13 @@ Here are some requests you can try: That's it! You've just created your first LoopBack 4 application! +### Bonus: Integrate with a REST based geo-coding service + +A typical REST API server needs to access data from a variety of sources, +including SOAP or REST services. Continue to the bonus section to learn how +LoopBack connectors make it super easy to fetch data from other services and +[enhance your Todo application with location-based reminders](todo-tutorial-geocoding-service.md). + ### More examples and tutorials Eager to continue learning about LoopBack 4? Check out our @@ -127,3 +134,6 @@ creating your own custom components, sequences and more! ### Navigation Previous step: [Add a controller](todo-tutorial-controller.md) + +Next step: +[Integrate with a geo-coding service](todo-tutorial-geocoding-service.md) diff --git a/examples/todo/README.md b/examples/todo/README.md index 258f2e8e1276..50e0d1f8ba09 100644 --- a/examples/todo/README.md +++ b/examples/todo/README.md @@ -40,6 +40,8 @@ section. 5. [Add a repository](http://loopback.io/doc/en/lb4/todo-tutorial-repository.html) 6. [Add a controller](http://loopback.io/doc/en/lb4/todo-tutorial-controller.html) 7. [Putting it all together](http://loopback.io/doc/en/lb4/todo-tutorial-putting-it-together.html) +8. Bonus: + [Integrate with a geo-coding service](http://loopback.io/doc/en/lb4/todo-tutorial-geocoder-service.html) ## Try it out diff --git a/examples/todo/src/application.ts b/examples/todo/src/application.ts index e6005dd89ab1..8f530e17910c 100644 --- a/examples/todo/src/application.ts +++ b/examples/todo/src/application.ts @@ -42,6 +42,7 @@ export class TodoListApplication extends BootMixin( }; // TODO(bajtos) Services should be created and registered by @loopback/boot + // See https://github.com/strongloop/loopback-next/issues/1439 this.setupServices(); } @@ -51,6 +52,7 @@ export class TodoListApplication extends BootMixin( // TODO(bajtos) app.service should be provided either by core Application // class or a mixin provided by @loopback/service-proxy + // See https://github.com/strongloop/loopback-next/issues/1439 service(provider: Constructor>) { const key = `services.${provider.name.replace(/Provider$/, '')}`; this.bind(key).toProvider(provider); diff --git a/examples/todo/src/datasources/geocoder.datasource.ts b/examples/todo/src/datasources/geocoder.datasource.ts index 241a480f8496..93d713777cf9 100644 --- a/examples/todo/src/datasources/geocoder.datasource.ts +++ b/examples/todo/src/datasources/geocoder.datasource.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {inject} from '@loopback/core'; -import {juggler, DataSource} from '@loopback/repository'; +import {juggler, AnyObject} from '@loopback/repository'; const config = require('./geocoder.datasource.json'); export class GeocoderDataSource extends juggler.DataSource { @@ -12,7 +12,7 @@ export class GeocoderDataSource extends juggler.DataSource { constructor( @inject('datasources.config.geocoder', {optional: true}) - dsConfig: DataSource = config, + dsConfig: AnyObject = config, ) { dsConfig = Object.assign({}, dsConfig, { // A workaround for the current design flaw where inside our monorepo, diff --git a/examples/todo/src/datasources/index.ts b/examples/todo/src/datasources/index.ts index 425f38ab0545..4a50e9a24238 100644 --- a/examples/todo/src/datasources/index.ts +++ b/examples/todo/src/datasources/index.ts @@ -4,3 +4,4 @@ // License text available at https://opensource.org/licenses/MIT export * from './db.datasource'; +export * from './geocoder.datasource';