This repository has been archived by the owner on Nov 22, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
app.js
194 lines (167 loc) · 6.26 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
const app = require('express')();
const _ = require('lodash');
const Router = require('express').Router;
const adminLookup = require('pelias-wof-admin-lookup');
const Document = require( 'pelias-model' ).Document;
const fs = require('fs');
const path = require('path');
const morgan = require( 'morgan' );
const through = require( 'through2' );
// helper that converts a string to a number if parseable, throws error otherwise
function toFiniteNumber(field, value) {
if (!_.isEmpty(_.trim(value)) && _.isFinite(_.toNumber(value))) {
return _.toNumber(value);
}
throw `cannot parse ${field} as finite number`;
}
// validation middleware, additionally performs layer-specific checks
const validate = (req, res, next) => {
// there are probably better ways to organize this validation middleware
// validate that lat/lon are numbers and convert
try {
req.query.lat = toFiniteNumber('lat', req.query.lat);
req.query.lon = toFiniteNumber('lon', req.query.lon);
} catch (err) {
// if either lat or lon are not parseable as finite numbers, then bail early
res.setHeader('content-type', 'text/plain');
res.status(400).send(err);
// skip PiP lookup and output middlewares
return next('route');
}
if (!_.isString(req.query.name) || _.isEmpty(_.trim(req.query.name))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('name parameter is required');
// skip PiP lookup and output middlewares
return next('route');
}
if (!_.isString(req.query.id) || _.isEmpty(_.trim(req.query.id))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('id parameter is required');
// skip PiP lookup and output middlewares
return next('route');
}
if (req.params.layer === 'address') {
// address layer-specific tests:
// - house_number is required
// - street is required
if (_.isEmpty(_.trim(req.query.house_number))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('house_number parameter is required for address layer');
// skip PiP lookup and output middlewares
return next('route');
}
if (_.isEmpty(_.trim(req.query.street))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('street parameter is required for address layer');
// skip PiP lookup and output middlewares
return next('route');
}
} else if (req.params.layer === 'street') {
// street layer-specific tests:
// - house_number is not allowed
// - street is required
if (!_.isEmpty(req.query.house_number)) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('house_number parameter is not applicable for street layer');
// skip PiP lookup and output middlewares
return next('route');
}
if (_.isEmpty(_.trim(req.query.street))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('street parameter is required for street layer');
// skip PiP lookup and output middlewares
return next('route');
}
} else if (req.params.layer === 'venue') {
// venue layer-specific tests:
// - street is required when house_number is supplied
if (!_.isEmpty(req.query.house_number) && _.isEmpty(_.trim(req.query.street))) {
res.setHeader('content-type', 'text/plain');
res.status(400).send('house_number parameter is required when street is supplied for venue layer');
// skip PiP lookup and output middlewares
return next('route');
}
}
// both lat and lon are non-blank finite numbers, so validation step passes
next();
};
// generate an initial doc from source, layer, id, name, and address parts
const generate = (req, res, next) => {
const doc = new Document( req.params.source, req.params.layer, _.trim(req.query.id) );
doc.setName( 'default', _.trim(req.query.name) );
doc.setCentroid({
lat: req.query.lat,
lon: req.query.lon
});
if (!_.isEmpty(_.trim(req.query.house_number))) {
doc.setAddress('number', _.trim(req.query.house_number));
}
if (!_.isEmpty(_.trim(req.query.street))) {
doc.setAddress('street', _.trim(req.query.street));
}
if (!_.isEmpty(_.trim(req.query.postcode))) {
doc.setAddress('zip', _.trim(req.query.postcode));
}
req.query.doc = doc;
next();
};
// perform PiP lookup and populate administrative hierarchy when no errors
function lookup(pointInPoly) {
return (req, res, next) => {
// async PiP lookup, no layers
pointInPoly.lookup(req.query.doc.getCentroid(), undefined, (err, result) => {
if (err) {
// bail early if there's an error
res.setHeader('content-type', 'text/plain');
res.status(500).send(err);
// skip output middleware
return next('route');
}
// iterate the layers, adding administrative hierarchy id/name/abbreviation
_.keys(result).forEach((layer) => {
req.query.doc.addParent(
layer,
result[layer][0].name,
result[layer][0].id.toString(),
result[layer][0].abbr);
});
next();
});
};
}
// convert the doc to an Elasticsearch doc and output as JSON
const output = (req, res, next) => {
// success!
res.status(200).send(req.query.doc.toESDocument().data);
next();
};
// log the request
function log() {
morgan.token('url', (req, res) => {
return req.originalUrl;
});
return morgan('combined', {
stream: through( function write( ln, _, next ){
console.log( ln.toString().trim() );
next();
})
});
}
module.exports = (datapath) => {
// verify the WOF data structure first (must contain data and meta directories)
if (!['meta', 'data'].every(sub => fs.existsSync(path.join(datapath, sub)))) {
throw Error(`${datapath} does not contain Who's on First data`);
}
// setup the PiP resolver
const pointInPoly = adminLookup.resolver(datapath);
const router = new Router();
// steps for successful document synthesis:
// 1. validate all required parameters
// 2. generate the initial document
// 3. populate the administrative hierarchy from a PiP call
// 4. output the synthesized document
router.get('/synthesize/:source/:layer(venue|address|street)', validate, generate, lookup(pointInPoly), output);
// make sure that logging happens first
app.use(log(), router);
return app;
};