Files
kami-parse-server/src/PromiseRouter.js
Diamond Lewis a02014f557 Improve single schema cache (#7214)
* Initial Commit

* fix flaky test

* temporary set ci timeout

* turn off ci check

* fix postgres tests

* fix tests

* node flaky test

* remove improvements

* Update SchemaPerformance.spec.js

* fix tests

* revert ci

* Create Singleton Object

* properly clear cache testing

* Cleanup

* remove fit

* try PushController.spec

* try push test rewrite

* try push enqueue time

* Increase test timeout

* remove pg server creation test

* xit push tests

* more xit

* remove skipped tests

* Fix conflicts

* reduce ci timeout

* fix push tests

* Revert "fix push tests"

This reverts commit 05aba62f1cbbca7d5d3e80b9444529f59407cb56.

* improve initialization

* fix flaky tests

* xit flaky test

* Update CHANGELOG.md

* enable debug logs

* Update LogsRouter.spec.js

* create initial indexes in series

* lint

* horizontal scaling documentation

* Update Changelog

* change horizontalScaling db option

* Add enableSchemaHooks option

* move enableSchemaHooks to databaseOptions
2021-03-16 16:05:36 -05:00

211 lines
6.0 KiB
JavaScript

// 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 method = req.method;
const headers = req.headers;
log.logRequest({
method,
url,
headers,
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';
}
log.logResponse({ method, url, result });
var status = result.status || 200;
res.status(status);
if (result.headers) {
Object.keys(result.headers).forEach(header => {
res.set(header, result.headers[header]);
});
}
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;
}
}
res.json(result.response);
},
error => {
next(error);
}
)
.catch(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;
}