-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandler.ts
234 lines (212 loc) · 7.48 KB
/
handler.ts
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/**
* This script contains the AWS Lambda handler code for merging some number of geocoder instances
* together
* Dependencies are listed in package.json in the same folder.
* Notes:
* - Most of the folder contents is uploaded to AWS Lambda (see README.md for deploying).
*/
import Bugsnag from '@bugsnag/js'
import getGeocoder from '@opentripplanner/geocoder'
import { Geometry, FeatureCollection, GeoJsonProperties } from 'geojson'
import { OfflineResponse } from '@opentripplanner/geocoder/lib/apis/offline'
import {
cachedGeocoderRequest,
checkIfResultsAreSatisfactory,
convertQSPToGeocoderArgs,
makeQueryPeliasCompatible,
mergeResponses,
ServerlessCallbackFunction,
ServerlessEvent,
ServerlessResponse
} from './utils'
// This plugin must be imported via cjs to ensure its existence (typescript recommendation)
const BugsnagPluginAwsLambda = require('@bugsnag/plugin-aws-lambda')
const {
BACKUP_GEOCODERS,
BUGSNAG_NOTIFIER_KEY,
CHECK_NAME_DUPLICATES,
GEOCODERS
} = process.env
const POIS = require('./pois.json')
if (!GEOCODERS) {
throw new Error(
'Error: required configuration variable GEOCODERS not found! Ensure env.yml has been decrypted.'
)
}
const geocoders = JSON.parse(GEOCODERS)
const backupGeocoders = BACKUP_GEOCODERS && JSON.parse(BACKUP_GEOCODERS)
// Serverless is not great about null
const pois =
POIS && POIS !== 'null'
? (POIS as OfflineResponse).map((poi) => {
if (typeof poi.lat === 'string') {
poi.lat = parseFloat(poi.lat)
}
if (typeof poi.lon === 'string') {
poi.lon = parseFloat(poi.lon)
}
return poi
})
: []
if (geocoders.length !== backupGeocoders.length) {
throw new Error(
'Error: BACKUP_GEOCODERS is not set to the same length as GEOCODERS'
)
}
Bugsnag.start({
apiKey: BUGSNAG_NOTIFIER_KEY || '',
appType: 'pelias-stitcher-lambda-function',
appVersion: require('./package.json').version,
plugins: [BugsnagPluginAwsLambda],
releaseStage: process.env.STAGE
})
// This handler will wrap around the handler code
// and will report exceptions to Bugsnag automatically.
// For reference, see https://docs.bugsnag.com/platforms/javascript/aws-lambda/#usage
const bugsnagHandler = Bugsnag?.getPlugin('awsLambda')?.createHandler()
/**
* Makes a call to a Pelias Instance using secrets from the config file.
* Includes special query parameters needed for each type of server.
*
* Errors will automatically be caught by the bugsnag wrapper on the handler
* @param event Event from Serverless framework
* @param apiMethod Method to call on Pelias Server
* @returns Object containing Serverless response object including parsed JSON responses from both Pelias instances
*/
export const makeGeocoderRequests = async (
event: ServerlessEvent,
apiMethod: string
): Promise<ServerlessResponse> => {
// "Clean" the text parameter to ensure the user's query is understood by Pelias
if (event?.queryStringParameters?.text) {
event.queryStringParameters.text = makeQueryPeliasCompatible(
event.queryStringParameters.text
)
}
// Pelias has different layers, and so needs to ignore the layers parameter
// if it is present
const peliasQSP = { ...event.queryStringParameters }
delete peliasQSP.layers
// Run all requests in parallel
const uncheckedResponses: FeatureCollection[] = await Promise.all(
geocoders.map((geocoder) =>
cachedGeocoderRequest(getGeocoder(geocoder), apiMethod, {
...convertQSPToGeocoderArgs(event.queryStringParameters),
items: pois
})
)
)
// Check if responses are satisfactory, and re-do them if needed
const responses = await Promise.all(
uncheckedResponses.map(async (response, index) => {
// If backup geocoder is present, and the returned results are garbage, use the backup geocoder
// if one is configured. This request will not be cached
if (
backupGeocoders[index] &&
!checkIfResultsAreSatisfactory(
response,
event.queryStringParameters.text
)
) {
const backupGeocoder = getGeocoder(backupGeocoders[index])
return await backupGeocoder[apiMethod](
convertQSPToGeocoderArgs(event.queryStringParameters)
)
}
return response
})
)
const merged = responses.reduce<
FeatureCollection<Geometry, GeoJsonProperties>
>(
(prev, cur, idx) => {
if (idx === 0) return cur
return mergeResponses(
{ customResponse: cur, primaryResponse: prev },
// Default to true
CHECK_NAME_DUPLICATES !== 'false'
// TODO: use focus point here to pre-sort results? It's possible to grab
// the focus point by calling convertQSPToGeocoderArgs on event.queryStringParameters
)
},
// TODO: clean this reducer up. See https://github.com/ibi-group/pelias-stitch/pull/28#discussion_r1547582739
{ features: [], type: 'FeatureCollection' }
)
return {
body: JSON.stringify(merged),
/*
The third "standard" CORS header, Access-Control-Allow-Methods is not included here
following reccomendations in https://www.serverless.com/blog/cors-api-gateway-survival-guide/
This header is handled within AWS API Gateway, via the serverless CORS setting.
*/
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
statusCode: 200
}
}
/**
* Entirely matches the Pelias autocomplete endpoint. Merges 2 autocomplete responses together.
* See https://github.com/pelias/documentation/blob/master/autocomplete.md
*/
module.exports.autocomplete = bugsnagHandler(
async (
event: ServerlessEvent,
context: null,
callback: ServerlessCallbackFunction
): Promise<void> => {
const response = await makeGeocoderRequests(event, 'autocomplete')
callback(null, response)
}
)
/**
* Entirely matches the Pelias search endpoint. Merges 2 search responses together.
* See https://github.com/pelias/documentation/blob/master/search.md
*/
module.exports.search = bugsnagHandler(
async (
event: ServerlessEvent,
context: null,
callback: ServerlessCallbackFunction
): Promise<void> => {
const response = await makeGeocoderRequests(event, 'search')
callback(null, response)
}
)
/**
* Entirely matches the Pelias reverse endpoint. Merges 2 reverse responses together.
* See https://github.com/pelias/documentation/blob/master/reverse.md
*/
module.exports.reverse = bugsnagHandler(
async (
event: ServerlessEvent,
context: null,
callback: ServerlessCallbackFunction
): Promise<void> => {
let geocoderResponse = await getGeocoder(geocoders[0]).reverse(
convertQSPToGeocoderArgs(event.queryStringParameters)
)
if (!geocoderResponse && backupGeocoders[0]) {
geocoderResponse = await getGeocoder(backupGeocoders[0]).reverse(
convertQSPToGeocoderArgs(event.queryStringParameters)
)
}
geocoderResponse.label = geocoderResponse.name
callback(null, {
body: JSON.stringify([geocoderResponse]),
/*
The third "standard" CORS header, Access-Control-Allow-Methods is not included here
following reccomendations in https://www.serverless.com/blog/cors-api-gateway-survival-guide/
This header is handled within AWS API Gateway, via the serverless CORS setting.
*/
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
statusCode: 200
})
}
)