Skip to content

Commit

Permalink
feat(example-todo): add Geo to examples/todo
Browse files Browse the repository at this point in the history
Show how to use REST services (US Census Geocoder) in LB4 apps.
  • Loading branch information
bajtos committed Jun 18, 2018
1 parent 142c5ee commit b4a9a9e
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 57 deletions.
6 changes: 6 additions & 0 deletions docs/site/Calling-other-APIs-and-Web-Services.md
Expand Up @@ -56,6 +56,12 @@ const ds: juggler.DataSource = new juggler.DataSource({
});
```

Install the REST connector used by the new datasource:

```
$ npm install --save loopback-connector-rest
```

### Bind data sources to the context

```ts
Expand Down
13 changes: 8 additions & 5 deletions examples/todo/package.json
Expand Up @@ -7,7 +7,6 @@
"node": ">=8"
},
"scripts": {
"acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"",
"build": "npm run build:dist8 && npm run build:dist10",
"build:apidocs": "lb-apidocs",
"build:current": "lb-tsc",
Expand All @@ -23,9 +22,8 @@
"tslint": "lb-tslint",
"tslint:fix": "npm run tslint -- --fix",
"pretest": "npm run build:current",
"test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/acceptance/**/*.js\"",
"test": "lb-mocha \"DIST/test/*/**/*.js\"",
"test:dev": "lb-mocha --allow-console-logs DIST/test/**/*.js && npm run posttest",
"unit": "lb-mocha \"DIST/test/unit/**/*.js\"",
"verify": "npm pack && tar xf loopback-todo*.tgz && tree package && npm run clean",
"prestart": "npm run build:current",
"start": "node ."
Expand All @@ -46,12 +44,17 @@
"@loopback/openapi-v3": "^0.10.9",
"@loopback/openapi-v3-types": "^0.7.7",
"@loopback/repository": "^0.11.3",
"@loopback/rest": "^0.11.3"
"@loopback/rest": "^0.11.3",
"@loopback/service-proxy": "^0.5.5",
"loopback-connector-rest": "^3.1.1"
},
"devDependencies": {
"@loopback/build": "^0.6.8",
"@loopback/http-caching-proxy": "^0.2.3",
"@loopback/testlab": "^0.10.7",
"@types/node": "^10.1.1"
"@types/lodash": "^4.14.109",
"@types/node": "^10.1.1",
"lodash": "^4.17.10"
},
"keywords": [
"loopback",
Expand Down
17 changes: 16 additions & 1 deletion examples/todo/src/application.ts
Expand Up @@ -3,9 +3,10 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {ApplicationConfig} from '@loopback/core';
import {ApplicationConfig, Provider, Constructor} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
import {MySequence} from './sequence';
import {GeocoderServiceProvider} from './services';

/* tslint:disable:no-unused-variable */
// Binding and Booter imports are required to infer types for BootMixin!
Expand Down Expand Up @@ -39,5 +40,19 @@ export class TodoListApplication extends BootMixin(
nested: true,
},
};

// TODO(bajtos) Services should be created and registered by @loopback/boot
this.setupServices();
}

setupServices() {
this.service(GeocoderServiceProvider);
}

// TODO(bajtos) app.service should be provided either by core Application
// class or a mixin provided by @loopback/service-proxy
service<T>(provider: Constructor<Provider<T>>) {
const key = `services.${provider.name.replace(/Provider$/, '')}`;
this.bind(key).toProvider(provider);
}
}
31 changes: 23 additions & 8 deletions examples/todo/src/controllers/todo.controller.ts
Expand Up @@ -3,22 +3,27 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {inject} from '@loopback/core';
import {repository} from '@loopback/repository';
import {TodoRepository} from '../repositories';
import {Todo} from '../models';
import {
HttpErrors,
post,
param,
requestBody,
del,
get,
put,
param,
patch,
del,
post,
put,
requestBody,
} from '@loopback/rest';
import {Todo} from '../models';
import {TodoRepository} from '../repositories';
import {GeocoderService} from '../services';

export class TodoController {
constructor(@repository(TodoRepository) protected todoRepo: TodoRepository) {}
constructor(
@repository(TodoRepository) protected todoRepo: TodoRepository,
@inject('services.GeocoderService') protected geoService: GeocoderService,
) {}

@post('/todos')
async createTodo(@requestBody() todo: Todo) {
Expand All @@ -27,6 +32,16 @@ export class TodoController {
if (!todo.title) {
throw new HttpErrors.BadRequest('title is required');
}

if (todo.remindAtAddress) {
// TODO(bajtos) handle "address not found"
const geo = await this.geoService.geocode(todo.remindAtAddress);
// Encode the coordinates as "lat,lng" (Google Maps API format). See also
// https://stackoverflow.com/q/7309121/69868
// https://gis.stackexchange.com/q/7379
todo.remindAtGeo = `${geo[0].y},${geo[0].x}`;
}

return await this.todoRepo.create(todo);
}

Expand Down
27 changes: 27 additions & 0 deletions examples/todo/src/datasources/geocoder.datasource.json
@@ -0,0 +1,27 @@
{
"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"]
}
}
]
}

26 changes: 26 additions & 0 deletions examples/todo/src/datasources/geocoder.datasource.ts
@@ -0,0 +1,26 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {inject} from '@loopback/core';
import {juggler, DataSource} 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 = Object.assign({}, dsConfig, {
// A workaround for the current design flaw where inside our monorepo,
// packages/service-proxy/node_modules/loopback-datasource-juggler
// cannot see/load the connector from
// examples/todo/node_modules/loopback-connector-rest
connector: require('loopback-connector-rest'),
});
super(dsConfig);
}
}
15 changes: 15 additions & 0 deletions examples/todo/src/models/todo.model.ts
Expand Up @@ -29,7 +29,22 @@ export class Todo extends Entity {
})
isComplete: boolean;

@property({
type: 'string',
})
remindAtAddress: string; // address,city,zipcode

// TODO(bajtos) Use LoopBack's GeoPoint type here
@property({
type: 'string',
})
remindAtGeo: string; // latitude,longitude

getId() {
return this.id;
}

constructor(data?: Partial<Todo>) {
super(data);
}
}
35 changes: 35 additions & 0 deletions examples/todo/src/services/geocoder.service.ts
@@ -0,0 +1,35 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

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> {
constructor(
@inject('datasources.geocoder')
protected datasource: juggler.DataSource = new GeocoderDataSource(),
) {}

value(): GeocoderService {
return getService(this.datasource);
}
}
6 changes: 6 additions & 0 deletions examples/todo/src/services/index.ts
@@ -0,0 +1,6 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './geocoder.service';
85 changes: 53 additions & 32 deletions examples/todo/test/acceptance/application.acceptance.ts
Expand Up @@ -4,44 +4,32 @@
// License text available at https://opensource.org/licenses/MIT

import {createClientForHandler, expect, supertest} from '@loopback/testlab';
import {RestServer} from '@loopback/rest';
import {TodoListApplication} from '../../src/application';
import {TodoRepository} from '../../src/repositories/';
import {givenTodo} from '../helpers';
import {Todo} from '../../src/models/';
import {TodoRepository} from '../../src/repositories/';
import {
HttpCachingProxy,
aLocation,
getProxiedGeoCoderConfig,
givenCachingProxy,
givenTodo,
} from '../helpers';

describe('Application', () => {
let app: TodoListApplication;
let server: RestServer;
let client: supertest.SuperTest<supertest.Test>;
let todoRepo: TodoRepository;

before(givenAnApplication);
before(async () => {
await app.boot();
let cachingProxy: HttpCachingProxy;
before(async () => (cachingProxy = await givenCachingProxy()));
after(() => cachingProxy.stop());

/**
* Override DataSource to not write to file for testing. Since we aren't
* persisting data to file and each injection normally instatiates a new
* instance, we must change the BindingScope to a singleton so only one
* instance is created and used for all injections (preserving access to
* the same memory space).
*/
app.bind('datasources.config.db').to({
name: 'db',
connector: 'memory',
});
before(givenRunningApplicationWithCustomConfiguration);
after(() => app.stop());

// Start Application
await app.start();
});
before(givenARestServer);
before(givenTodoRepository);
before(() => {
client = createClientForHandler(server.requestHandler);
});
after(async () => {
await app.stop();
client = createClientForHandler(app.requestHandler);
});

it('creates a todo', async () => {
Expand All @@ -50,17 +38,34 @@ describe('Application', () => {
.post('/todos')
.send(todo)
.expect(200);
expect(response.body).to.containDeep(todo);
const result = await todoRepo.findById(response.body.id);
expect(result).to.containDeep(todo);
});

it('creates an address-based reminder', async () => {
const todo = givenTodo({remindAtAddress: aLocation.address});
const response = await client
.post('/todos')
.send(todo)
.expect(200);
todo.remindAtGeo = aLocation.geostring;

expect(response.body).to.containEql(todo);

const result = await todoRepo.findById(response.body.id);
expect(result).to.containEql(todo);
});

it('gets a todo by ID', async () => {
const todo = await givenTodoInstance();
await client
const result = await client
.get(`/todos/${todo.id}`)
.send()
.expect(200, todo);
.expect(200);
// Remove any undefined properties that cannot be represented in JSON/REST
const expected = JSON.parse(JSON.stringify(todo));
expect(result.body).to.deepEqual(expected);
});

it('replaces the todo by ID', async () => {
Expand Down Expand Up @@ -118,16 +123,32 @@ describe('Application', () => {
- keep them DRY (who wants to write the same stuff over and over?)
============================================================================
*/
function givenAnApplication() {

async function givenRunningApplicationWithCustomConfiguration() {
app = new TodoListApplication({
rest: {
port: 0,
},
});
}

async function givenARestServer() {
server = await app.getServer(RestServer);
await app.boot();

/**
* Override default config for DataSource for testing so we don't write
* test data to file when using the memory connector.
*/
app.bind('datasources.config.db').to({
name: 'db',
connector: 'memory',
});

// Override Geocoder datasource to use a caching proxy to speed up tests.
app
.bind('datasources.config.geocoder')
.to(getProxiedGeoCoderConfig(cachingProxy));

// Start Application
await app.start();
}

async function givenTodoRepository() {
Expand Down

0 comments on commit b4a9a9e

Please sign in to comment.