forked from parse-community/parse-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
PromiseRouter.js
207 lines (186 loc) · 6.02 KB
/
PromiseRouter.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
195
196
197
198
199
200
201
202
203
204
205
206
207
// A router that is based on promises rather than req/res/next.
// This is intended to replace the use of express.Router to handle
// subsections of the API surface.
// This will make it easier to have methods like 'batch' that
// themselves use our routing information, without disturbing express
// components that external developers may be modifying.
import Parse from 'parse/node';
import express from 'express';
import log from './logger';
import {inspect} from 'util';
const Layer = require('express/lib/router/layer');
function validateParameter(key, value) {
if (key == 'className') {
if (value.match(/_?[A-Za-z][A-Za-z_0-9]*/)) {
return value;
}
} else if (key == 'objectId') {
if (value.match(/[A-Za-z0-9]+/)) {
return value;
}
} else {
return value;
}
}
export default class PromiseRouter {
// Each entry should be an object with:
// path: the path to route, in express format
// method: the HTTP method that this route handles.
// Must be one of: POST, GET, PUT, DELETE
// handler: a function that takes request, and returns a promise.
// Successful handlers should resolve to an object with fields:
// status: optional. the http status code. defaults to 200
// response: a json object with the content of the response
// location: optional. a location header
constructor(routes = [], appId) {
this.routes = routes;
this.appId = appId;
this.mountRoutes();
}
// Leave the opportunity to
// subclasses to mount their routes by overriding
mountRoutes() {}
// Merge the routes into this one
merge(router) {
for (var route of router.routes) {
this.routes.push(route);
}
}
route(method, path, ...handlers) {
switch(method) {
case 'POST':
case 'GET':
case 'PUT':
case 'DELETE':
break;
default:
throw 'cannot route method: ' + method;
}
let handler = handlers[0];
if (handlers.length > 1) {
handler = function(req) {
return handlers.reduce((promise, handler) => {
return promise.then(() => {
return handler(req);
});
}, Promise.resolve());
}
}
this.routes.push({
path: path,
method: method,
handler: handler,
layer: new Layer(path, null, handler)
});
}
// Returns an object with:
// handler: the handler that should deal with this request
// params: any :-params that got parsed from the path
// Returns undefined if there is no match.
match(method, path) {
for (var route of this.routes) {
if (route.method != method) {
continue;
}
const layer = route.layer || new Layer(route.path, null, route.handler);
const match = layer.match(path);
if (match) {
const params = layer.params;
Object.keys(params).forEach((key) => {
params[key] = validateParameter(key, params[key]);
});
return {params: params, handler: route.handler};
}
}
}
// Mount the routes on this router onto an express app (or express router)
mountOnto(expressApp) {
this.routes.forEach((route) => {
const method = route.method.toLowerCase();
const handler = makeExpressHandler(this.appId, route.handler);
expressApp[method].call(expressApp, route.path, handler);
});
return expressApp;
}
expressRouter() {
return this.mountOnto(express.Router());
}
tryRouteRequest(method, path, request) {
var match = this.match(method, path);
if (!match) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
'cannot route ' + method + ' ' + path);
}
request.params = match.params;
return new Promise((resolve, reject) => {
match.handler(request).then(resolve, reject);
});
}
}
// A helper function to make an express handler out of a a promise
// handler.
// Express handlers should never throw; if a promise handler throws we
// just treat it like it resolved to an error.
function makeExpressHandler(appId, promiseHandler) {
return function(req, res, next) {
try {
const url = maskSensitiveUrl(req);
const body = Object.assign({}, req.body);
const stringifiedBody = JSON.stringify(body, null, 2);
log.verbose(`REQUEST for [${req.method}] ${url}: ${stringifiedBody}`, {
method: req.method,
url: url,
headers: req.headers,
body: body
});
promiseHandler(req).then((result) => {
if (!result.response && !result.location && !result.text) {
log.error('the handler did not include a "response" or a "location" field');
throw 'control should not get here';
}
const stringifiedResponse = JSON.stringify(result, null, 2);
log.verbose(
`RESPONSE from [${req.method}] ${url}: ${stringifiedResponse}`,
{result: result}
);
var status = result.status || 200;
res.status(status);
if (result.text) {
res.send(result.text);
return;
}
if (result.location) {
res.set('Location', result.location);
// Override the default expressjs response
// as it double encodes %encoded chars in URL
if (!result.response) {
res.send('Found. Redirecting to ' + result.location);
return;
}
}
if (result.headers) {
Object.keys(result.headers).forEach((header) => {
res.set(header, result.headers[header]);
})
}
res.json(result.response);
}, (e) => {
log.error(`Error generating response. ${inspect(e)}`, {error: e});
next(e);
});
} catch (e) {
log.error(`Error handling request: ${inspect(e)}`, {error: e});
next(e);
}
}
}
function maskSensitiveUrl(req) {
let maskUrl = req.originalUrl.toString();
const shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
&& !req.originalUrl.includes('classes');
if (shouldMaskUrl) {
maskUrl = log.maskSensitiveUrl(maskUrl);
}
return maskUrl;
}