-
Notifications
You must be signed in to change notification settings - Fork 1.1k
docs: add geo-coding service to Todo tutorial #1441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do these new files do?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in be80a19 |
||
|
|
||
| - `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<GeoPoint[]>; | ||
| } | ||
|
|
||
| export class GeocoderServiceProvider implements Provider<GeocoderService> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder why we don't use At the moment,
WDYT?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I started to look into testing services, I found See this integration test for an example: I am of the opinion that integration tests should not be using Application, Context and Dependency Injection, instead they should instantiate all classes directly from code. This makes refactorings easier, because all dependencies and couplings are explicitly expressed in code and IDE tooling can update them accordingly. (Compare that to acceptance tests where a lot of configuration is based on convention and string names.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we did not need to tweak datasource settings to add caching proxy, the code would become even more simple: const dataSource = new GeocoderDataSource();
service = new GeocoderServiceProvider(dataSource).value();Originally, before support for booting datasources was added, this used to be a one-liner. I think we may be able to get there using default arguments in the current design too. service = new GeocoderServiceProvider().value();
I am pretty happy about the current design, to be honest.
In my mind, a Service (or a Service Proxy) is similar to a Repository. @raymondfeng thoughts?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am going to land this pull request. If there are any changes identified in the outcome of this discussion, then I'll open a new pull request to address them. Such changes are likely to affect the following places:
|
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want a place holder for this section until the boot feature can be completed?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean by a placeholder? IMO, the content of the tutorial should describe steps that people can do right now (as of DP3). The pull request adding support for automatic loading of services should update
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad, I think I had this mistaken with something else :p |
||
| [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<T>(provider: Constructor<Provider<T>>) { | ||
| 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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be adding a link to the geoservice section as a 'next step' if it's a bonus?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, why not? The reader finished the core tutorial and the next step is to look at bonus sections. |
||
| [Integrate with a geo-coding service](todo-tutorial-geocoding-service.md) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these prompts were defaulted, I think we should still add in the default choices after the colons
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default choice is an empty string.