Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
255 changes: 255 additions & 0 deletions docs/site/todo-tutorial-geocoding-service.md
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:
Copy link
Contributor

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

Copy link
Member Author

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.

? 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do these new files do?

Copy link
Member Author

Choose a reason for hiding this comment

The 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> {
Copy link
Contributor

@raymondfeng raymondfeng Jun 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we don't use @serviceProxy - http://loopback.io/doc/en/lb4/Calling-other-APIs-and-web-services.html#declare-service-proxies-for-your-controller.

At the moment, services are backed/configured by datasources. If we want to promote service as the 1st class artifact, we probably should evaluate the possibility of dividing datasource into two types:

  • datasource
  • service

WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I started to look into testing services, I found @serviceProxy to make testing difficult, because tests cannot use the same code path for obtaining the service proxy instance as the production code uses.

See this integration test for an example:

https://github.com/strongloop/loopback-next/blob/9d82c9e67faba57a7d274f1231c2d11548dd7629/examples/todo/test/integration/services/geocoder.service.integration.ts#L37-L41

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.)

Copy link
Member Author

Choose a reason for hiding this comment

The 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();

At the moment, services are backed/configured by datasources. If we want to promote service as the 1st class artifact, we probably should evaluate the possibility of dividing datasource into two types:

  • datasource
  • service

I am pretty happy about the current design, to be honest.

  • A datasource is a long-living singleton-like object that manages state shared by all incoming HTTP requests - PersitedModel definitions, connection pool, etc.
  • A Repository or a Service is a lightweight cheap-to-create facade exposing user-friendly API to controllers, leveraging datasources to do the heavy lifting.

In my mind, a Service (or a Service Proxy) is similar to a Repository.

@raymondfeng thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The 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:

  • examples/todo implementation
  • "Calling other services" docs
  • "Testing your application" docs

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 examples/todo and this tutorial section to match the new simplified approach.

Copy link
Contributor

Choose a reason for hiding this comment

The 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)
10 changes: 10 additions & 0 deletions docs/site/todo-tutorial-putting-it-together.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -127,3 +134,6 @@ creating your own custom components, sequences and more!
### Navigation

Previous step: [Add a controller](todo-tutorial-controller.md)

Next step:
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
2 changes: 2 additions & 0 deletions examples/todo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions examples/todo/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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<T>(provider: Constructor<Provider<T>>) {
const key = `services.${provider.name.replace(/Provider$/, '')}`;
this.bind(key).toProvider(provider);
Expand Down
4 changes: 2 additions & 2 deletions examples/todo/src/datasources/geocoder.datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
// 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 {
static dataSourceName = 'geocoder';

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,
Expand Down
1 change: 1 addition & 0 deletions examples/todo/src/datasources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './db.datasource';
export * from './geocoder.datasource';