1
0
Fork 0
arangodb/js/server/modules/@arangodb/foxx/legacy/request_context.js

477 lines
14 KiB
JavaScript

'use strict';
// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2013-2014 triAGENS GmbH, Cologne, Germany
// / Copyright 2015 ArangoDB GmbH, Cologne, Germany
// /
// / Licensed under the Apache License, Version 2.0 (the "License")
// / you may not use this file except in compliance with the License.
// / You may obtain a copy of the License at
// /
// / http://www.apache.org/licenses/LICENSE-2.0
// /
// / Unless required by applicable law or agreed to in writing, software
// / distributed under the License is distributed on an "AS IS" BASIS,
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// / See the License for the specific language governing permissions and
// / limitations under the License.
// /
// / Copyright holder is ArangoDB GmbH, Cologne, Germany
// /
// / @author Lucas Dohmen
// / @author Michael Hackstein
// / @author Alan Plum
// //////////////////////////////////////////////////////////////////////////////
const SwaggerDocs = require('@arangodb/foxx/legacy/swaggerDocs').Docs;
const joi = require('joi');
const _ = require('lodash');
const internal = require('@arangodb/foxx/legacy/internals');
const toJSONSchema = require('@arangodb/foxx/legacy/schema').toJSONSchema;
const is = require('@arangodb/is');
const UnprocessableEntity = require('http-errors').UnprocessableEntity;
const UnauthorizedError = require('@arangodb/foxx/legacy/authentication').UnauthorizedError;
function createBodyParamExtractor (rootElement, paramName, allowInvalid) {
var extractElement;
if (rootElement) {
extractElement = function (req) {
return req.body()[paramName];
};
} else {
extractElement = function (req) {
return req.body();
};
}
if (!allowInvalid) {
return extractElement;
}
return function (req) {
try {
return extractElement(req);
} catch (e) {
return {};
}
};
}
function createModelInstantiator (Model, allowInvalid) {
var multiple = is.array(Model);
Model = multiple ? Model[0] : Model;
var instantiate = function (raw) {
if (!allowInvalid) {
raw = validateOrThrow(raw, Model.prototype.schema, allowInvalid);
}
return new Model(raw);
};
if (!multiple) {
return instantiate;
}
return function (raw) {
return _.map(raw, instantiate);
};
}
function isJoi (schema) {
if (!schema || typeof schema !== 'object' || is.array(schema)) {
return false;
}
if (schema.isJoi) { // shortcut for pure joi schemas
return true;
}
return Object.keys(schema).some(function (key) {
return schema[key].isJoi;
});
}
function validateOrThrow (raw, schema, allowInvalid, validateOptions) {
if (!isJoi(schema)) {
return raw;
}
var result = joi.validate(raw, schema, validateOptions);
if (result.error && !allowInvalid) {
throw new UnprocessableEntity(result.error.message.replace(/^"value"/, 'Request body'));
}
return result.value;
}
class RequestContext {
// //////////////////////////////////////////////////////////////////////////////
// / JSF_foxx_RequestContext_initializer
// / @brief Context of a Request Definition
// /
// / Used for documenting and constraining the routes.
// //////////////////////////////////////////////////////////////////////////////
constructor (executionBuffer, models, route, path, rootElement, constraints, extensions) {
this.path = path;
this.route = route;
this.typeToRegex = {
int: '/[0-9]+/',
integer: '/[0-9]+/',
string: '/[^/]+/'
};
this.rootElement = rootElement;
this.constraints = constraints;
this.docs = new SwaggerDocs(this.route.docs, models);
var attr;
var extensionWrapper = function (scope, func) {
return function () {
func.apply(this, arguments);
return this;
}.bind(scope);
};
for (attr in extensions) {
if (extensions.hasOwnProperty(attr)) {
this[attr] = extensionWrapper(this, extensions[attr]);
}
}
executionBuffer.applyEachFunction(this);
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_pathParam
// //////////////////////////////////////////////////////////////////////////////
pathParam (paramName, attributes) {
var url = this.route.url,
urlConstraint = url.constraint || {},
type = attributes.type,
required = attributes.required,
description = attributes.description,
constraint, regexType, cfg;
if (attributes.isJoi) {
type = attributes;
required = undefined;
description = undefined;
}
constraint = type;
regexType = type;
if (type) {
if (typeof required === 'boolean') {
constraint = required ? constraint.required() : constraint.optional();
}
if (typeof description === 'string') {
constraint = constraint.description(description);
}
this.constraints.urlParams[paramName] = constraint;
cfg = constraint.describe();
if (is.array(cfg)) {
cfg = cfg[0];
type = 'string';
} else {
type = cfg.type;
}
required = Boolean(cfg.flags && cfg.flags.presence === 'required');
description = cfg.description;
if (
type === 'number' &&
_.isArray(cfg.rules) &&
_.some(cfg.rules, function (rule) {
return rule.name === 'integer';
})
) {
type = 'integer';
}
if (_.has(this.typeToRegex, type)) {
regexType = type;
} else {
regexType = 'string';
}
}
urlConstraint[paramName] = this.typeToRegex[regexType];
if (!urlConstraint[paramName]) {
throw new Error('Illegal attribute type: ' + regexType);
}
this.route.url = internal.constructUrlObject(url.match, urlConstraint, url.methods[0]);
this.docs.addPathParam(paramName, description, type, required);
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_queryParam
// //////////////////////////////////////////////////////////////////////////////
queryParam (paramName, attributes) {
var type = attributes.type,
required = attributes.required,
description = attributes.description,
allowMultiple = attributes.allowMultiple,
constraint, cfg;
if (attributes.isJoi) {
type = attributes;
required = undefined;
description = undefined;
allowMultiple = undefined;
}
constraint = type;
if (type) {
if (typeof required === 'boolean') {
constraint = required ? constraint.required() : constraint.optional();
}
if (typeof description === 'string') {
constraint = constraint.description(description);
}
if (typeof allowMultiple === 'boolean') {
constraint = constraint.meta({allowMultiple: allowMultiple});
}
this.constraints.queryParams[paramName] = constraint;
cfg = constraint.describe();
if (is.array(cfg)) {
cfg = cfg[0];
type = 'string';
} else {
type = cfg.type;
}
required = Boolean(cfg.flags && cfg.flags.presence === 'required');
description = cfg.description;
if (cfg.meta) {
if (!is.array(cfg.meta)) {
cfg.meta = [cfg.meta];
}
_.each(cfg.meta, function (meta) {
if (meta && typeof meta.allowMultiple === 'boolean') {
allowMultiple = meta.allowMultiple;
}
});
}
if (
type === 'number' &&
_.isArray(cfg.rules) &&
_.some(cfg.rules, function (rule) {
return rule.name === 'integer';
})
) {
type = 'integer';
}
}
this.docs.addQueryParam(
paramName,
description,
type,
required,
Boolean(allowMultiple)
);
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_bodyParam
// //////////////////////////////////////////////////////////////////////////////
bodyParam (paramName, attributes) {
var type = attributes.type,
description = attributes.description,
allowInvalid = attributes.allowInvalid,
validateOptions = {},
cfg, construct;
if (attributes.isJoi) {
type = attributes;
description = undefined;
allowInvalid = undefined;
}
if (!type) {
construct = function (raw) {
return raw;
};
} else if (typeof type === 'function' || is.array(type)) {
// assume ModelOrSchema is a Foxx Model
construct = createModelInstantiator(type, allowInvalid);
} else {
if (!type.isJoi) {
type = joi.object().keys(type).required();
}
if (typeof allowInvalid === 'boolean') {
type = type.meta({allowInvalid: allowInvalid});
}
if (typeof description === 'string') {
type = type.description(description);
}
cfg = type.describe();
description = cfg.description;
if (cfg.meta) {
if (!is.array(cfg.meta)) {
cfg.meta = [cfg.meta];
}
_.each(cfg.meta, function (meta) {
if (meta && typeof meta.allowInvalid === 'boolean') {
allowInvalid = meta.allowInvalid;
}
});
}
if (cfg.options) {
if (!is.array(cfg.options)) {
cfg.options = [cfg.options];
}
_.each(cfg.options, function (options) {
Object.assign(validateOptions, options);
});
}
construct = function (raw) {
return validateOrThrow(raw, type, allowInvalid, validateOptions);
};
}
this.docs.addBodyParam(
paramName,
description,
toJSONSchema(paramName, is.array(type) ? type[0] : type)
);
this.route.action.bodyParams.push({
extract: createBodyParamExtractor(this.rootElement, paramName, allowInvalid),
paramName: paramName,
construct: construct
});
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_summary
// //////////////////////////////////////////////////////////////////////////////
summary (summary) {
if (summary.length > 8192) {
throw new Error("Summary can't be longer than 8192 characters");
}
this.docs.addSummary(summary);
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_notes
// //////////////////////////////////////////////////////////////////////////////
notes () {
var notes = Array.prototype.join.call(arguments, '\n');
this.docs.addNotes(notes);
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_errorResponse
// //////////////////////////////////////////////////////////////////////////////
errorResponse (errorClass, code, reason, errorHandler) {
this.route.action.errorResponses.push({
errorClass: errorClass,
code: code,
reason: reason,
errorHandler: errorHandler
});
this.route.docs.errorResponses.push({code: code, reason: reason});
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_onlyIf
// //////////////////////////////////////////////////////////////////////////////
onlyIf (check) {
this.route.action.checks.push({
check: check
});
return this;
}
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContext_onlyIfAuthenticated
// //////////////////////////////////////////////////////////////////////////////
onlyIfAuthenticated (code, reason) {
var check;
check = function (req) {
if (
!(req.session && req.session.get('uid')) // new and shiny
&& !(req.user && req.currentSession) // old and busted
) {
throw new UnauthorizedError();
}
};
if (is.notExisty(code)) {
code = 401;
}
if (is.notExisty(reason)) {
reason = 'Not authorized';
}
this.onlyIf(check);
this.errorResponse(UnauthorizedError, code, reason);
return this;
}
}
class RequestContextBuffer {
constructor () {
this.applyChain = [];
}
}
Object.assign(RequestContextBuffer.prototype, {
applyEachFunction(target) {
_.each(this.applyChain, function (x) {
target[x.functionName].apply(target, x.argumentList);
});
}
});
_.each([
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContextBuffer_pathParam
// //////////////////////////////////////////////////////////////////////////////
'pathParam',
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContextBuffer_queryParam
// //////////////////////////////////////////////////////////////////////////////
'queryParam',
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContextBuffer_errorResponse
// //////////////////////////////////////////////////////////////////////////////
'errorResponse',
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContextBuffer_onlyIf
// //////////////////////////////////////////////////////////////////////////////
'onlyIf',
// //////////////////////////////////////////////////////////////////////////////
// / @brief was docuBlock JSF_foxx_RequestContextBuffer_onlyIfAuthenticated
// //////////////////////////////////////////////////////////////////////////////
'onlyIfAuthenticated'
], function (functionName) {
Object.assign(RequestContextBuffer.prototype[functionName] = function () {
this.applyChain.push({
functionName: functionName,
argumentList: arguments
});
return this;
});
});
RequestContext.Buffer = RequestContextBuffer;
module.exports = exports = RequestContext;
exports.RequestContext = RequestContext;
exports.RequestContextBuffer = RequestContextBuffer;