1
0
Fork 0

Implement request/response body handling

This commit is contained in:
Alan Plum 2016-01-18 21:00:52 +01:00
parent a5ffbcf736
commit 8d2cc4041d
No known key found for this signature in database
GPG Key ID: 8ED72A9A323B6EFD
9 changed files with 757 additions and 310 deletions

View File

@ -0,0 +1,161 @@
'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief FoxxContext type
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2015 triagens 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 triAGENS GmbH, Cologne, Germany
///
/// @author Alan Plum
/// @author Copyright 2015-2016, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const internal = require('internal');
const mimeTypes = require('mime-types');
module.exports = class FoxxContext {
constructor(service) {
this.service = service;
this.argv = [];
}
use(path, router) {
this.service.router.use(path, router);
}
registerType(type, def) {
assert(
(
type instanceof RegExp
|| typeof type === 'string'
|| typeof type === 'function'
),
'Type name must be a string, function or RegExp'
);
type = mimeTypes.lookup(type) || type;
assert(
typeof def === 'object',
'Type handler definition must be an object'
);
def = _.clone(def);
assert(
!def.forClient || typeof def.forClient === 'function',
`Type forClient handler must be a function, not ${typeof def.forClient}`
);
assert(
!def.fromClient || typeof def.fromClient === 'function',
`Type fromClient handler must be a function, not ${typeof def.fromClient}`
);
if (!def.fromClient && typeof def.toClient === 'function') {
console.log(
`Found unexpected "toClient" method on type handler for "${type}".`
+ ' Did you mean "forClient"?'
);
}
this.service.types.set(type, def);
}
fileName(filename) {
return fs.safeJoin(this.basePath, filename);
}
file(filename, encoding) {
return fs.readFileSync(this.fileName(filename), encoding);
}
path(name) {
return path.join(this.basePath, name);
}
collectionName(name) {
let fqn = (
this.collectionPrefix
+ name.replace(/[^a-z0-9]/ig, '_').replace(/(^_+|_+$)/g, '').substr(0, 64)
);
if (!fqn.length) {
throw new Error(`Cannot derive collection name from "${name}"`);
}
return fqn;
}
collection(name) {
return internal.db._collection(this.collectionName(name));
}
get basePath() {
return this.service.basePath;
}
get baseUrl() {
return `/_db/${encodeURIComponent(internal.db._name())}/${this.service.mount.slice(1)}`;
}
get collectionPrefix() {
return this.service.collectionPrefix;
}
get mount() {
return this.service.mount;
}
get name() {
return this.service.name;
}
get version() {
return this.service.version;
}
get manifest() {
return this.service.manifest;
}
get isDevelopment() {
return this.service.isDevelopment;
}
get isProduction() {
return !this.isDevelopment;
}
get options() {
return this.service.options;
}
get configuration() {
return this.service.configuration;
}
get dependencies() {
return this.service.dependencies;
}
};

View File

@ -30,15 +30,23 @@ const _ = require('lodash');
const fs = require('fs');
const vary = require('vary');
const statuses = require('statuses');
const mediaTyper = require('media-typer');
const mimeTypes = require('mime-types');
const typeIs = require('type-is');
const contentDisposition = require('content-disposition');
const guessContentType = require('@arangodb').guessContentType;
const addCookie = require('@arangodb/actions').addCookie;
const crypto = require('@arangodb/crypto');
const MIME_BINARY = 'application/octet-stream';
const MIME_JSON = 'application/json; charset=utf-8';
module.exports = class SyntheticResponse {
constructor(res) {
constructor(res, context) {
this._raw = res;
this._responses = new Map();
this.context = context;
}
// Node compat
@ -85,26 +93,27 @@ module.exports = class SyntheticResponse {
return this;
}
write(body) {
const bodyIsBuffer = body && body instanceof Buffer;
if (!body) {
body = '';
} else if (!bodyIsBuffer) {
if (typeof body === 'object') {
body = JSON.stringify(body);
write(data) {
const bodyIsBuffer = this.body instanceof Buffer;
const dataIsBuffer = data instanceof Buffer;
if (!data) {
data = '';
} else if (!dataIsBuffer) {
if (typeof data === 'object') {
data = JSON.stringify(data);
} else {
body = String(body);
data = String(data);
}
}
if (!this._raw.body) {
this._raw.body = body;
} else if (this._raw.body instanceof Buffer) {
if (!this.body) {
this._raw.body = data;
} else if (bodyIsBuffer || dataIsBuffer) {
this._raw.body = Buffer.concat(
this._raw.body,
bodyIsBuffer ? body : new Buffer(body)
bodyIsBuffer ? this.body : new Buffer(this.body),
dataIsBuffer ? data : new Buffer(data)
);
} else {
this._raw.body += body;
this.body += data;
}
return this;
}
@ -127,7 +136,7 @@ module.exports = class SyntheticResponse {
json(value) {
if (!this._raw.contentType) {
this._raw.contentType = 'application/json';
this._raw.contentType = MIME_JSON;
}
this._raw.body = JSON.stringify(value);
return this;
@ -141,33 +150,13 @@ module.exports = class SyntheticResponse {
if (status === 'permanent') {
status = 301;
}
if (status || !this._raw.responseCode) {
this._raw.responseCode = status || 302;
if (status || !this.statusCode) {
this.statusCode = status || 302;
}
this.setHeader('location', path);
return this;
}
send(body) {
if (!body) {
body = '';
}
let impliedType = 'text/html';
if (body instanceof Buffer) {
impliedType = 'application/octet-stream';
} else if (body && typeof body === 'object') {
body = JSON.stringify(body);
impliedType = 'application/json';
} else if (typeof body !== 'string') {
body = String(body);
}
if (impliedType && !this._raw.contentType) {
this._raw.contentType = impliedType;
}
this._raw.body = body;
return this;
}
sendFile(filename, opts) {
if (!opts) {
opts = {};
@ -178,7 +167,7 @@ module.exports = class SyntheticResponse {
this.headers['last-modified'] = lastModified.toUTCString();
}
if (!this._raw.contentType) {
this._raw.contentType = guessContentType(filename, 'application/octet-stream');
this._raw.contentType = guessContentType(filename, MIME_BINARY);
}
return this;
}
@ -188,7 +177,7 @@ module.exports = class SyntheticResponse {
(Number(status) !== 306 && statuses[status])
|| String(status)
);
this._raw.responseCode = status;
this.statusCode = status;
this.send(message);
return this;
}
@ -205,7 +194,7 @@ module.exports = class SyntheticResponse {
}
status(statusCode) {
this._raw.responseCode = statusCode;
this.statusCode = statusCode;
return this;
}
@ -257,4 +246,69 @@ module.exports = class SyntheticResponse {
}
return this;
}
send(body, type) {
if (!type) {
type = 'auto';
}
let contentType;
const status = this.statusCode || 200;
const response = this._responses.get(status);
if (response) {
if (response.model && response.model.forClient) {
body = response.model.forClient(body);
}
if (type === 'auto' && response.contentTypes) {
type = response.contentTypes[0];
contentType = type;
}
}
if (type === 'auto') {
if (body instanceof Buffer) {
type = MIME_BINARY;
} else if (body && typeof body === 'object') {
type = 'json';
} else {
type = 'html';
}
}
type = mimeTypes.lookup(type) || type;
let handler;
for (const entry of this.context.service.types.entries()) {
const key = entry[0];
const value = entry[1];
let match;
if (key instanceof RegExp) {
match = type.test(key);
} else if (typeof key === 'function') {
match = key(type);
} else {
match = typeIs.is(key, type);
}
if (match && value.fromClient) {
handler = value;
break;
}
}
if (handler) {
const result = handler.forClient(body, this, mediaTyper.parse(contentType));
if (result.headers || result.data) {
this.set(result.headers);
body = result.data;
} else {
body = result;
}
}
this._raw.body = body;
this._raw.contentType = contentType || this._raw.contentType;
return this;
}
};

View File

@ -26,12 +26,16 @@
/// @author Copyright 2015-2016, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const joi = require('joi');
const assert = require('assert');
const mimeTypes = require('mime-types');
const mediaTyper = require('media-typer');
const joi2schema = require('joi-to-json-schema');
const joiToJsonSchema = require('joi-to-json-schema');
const tokenize = require('@arangodb/foxx/router/tokenize');
const MIME_JSON = 'application/json; charset=utf-8';
const MIME_BINARY = 'application/octet-stream';
const DEFAULT_BODY_SCHEMA = joi.object().optional().meta({allowInvalid: true});
const DEFAULT_ERROR_SCHEMA = joi.object().keys({
error: joi.allow(true).required(),
@ -68,89 +72,186 @@ module.exports = exports = class SwaggerContext {
this._pathTokens = tokenize(path, this);
}
header(name, type, description) {
this._headers.set(name, {type: type, description: description});
header(name, schema, description) {
this._headers.set(name, {schema: schema, description: description});
return this;
}
pathParam(name, type, description) {
this._pathParams.set(name, {type: type, description: description});
pathParam(name, schema, description) {
this._pathParams.set(name, {schema: schema, description: description});
return this;
}
queryParam(name, type, description) {
this._queryParams.set(name, {type: type, description: description});
queryParam(name, schema, description) {
this._queryParams.set(name, {schema: schema, description: description});
return this;
}
body(type, description) {
if (type === null) {
body(model, mimes, description) {
if (model === null) {
this._bodyParam = {};
} else {
let contentType = 'application/json; charset=utf-8';
if (type === 'binary') {
contentType = 'application/octet-stream';
} else if (typeof type !== 'object') {
contentType = mimeTypes.contentType(type) || type;
let multiple = false;
if (
typeof model === 'string'
|| (Array.isArray(model) && typeof model[0] === 'string')
) {
description = mimes;
mimes = model;
model = undefined;
}
const parsed = mediaTyper.parse(contentType);
this._bodyParam = {
type: type,
description: description,
contentType: {
header: contentType,
parsed: parsed,
mime: mediaTyper.format({
type: parsed.type,
subtype: parsed.subtype,
suffix: parsed.suffix
})
if (!Array.isArray(mimes)) {
mimes = mimes ? [mimes] : [];
}
if (Array.isArray(model)) {
if (model.length !== 1) {
throw new Error(
'Model must be a model or schema'
+ ' or an array containing exactly one model or schema.'
+ ' If you are trying to use multiple schemas'
+ ' try using joi.alternatives.'
);
}
model = model[0];
multiple = true;
}
if (!mimes.length && model) {
mimes.push(MIME_JSON);
}
const contentTypes = mimes.map(function (mime) {
if (mime === 'binary') {
mime = MIME_BINARY;
}
const contentType = mimeTypes.contentType(mime) || mime;
const parsed = mediaTyper.parse(contentType);
return mediaTyper.format(_.pick(parsed, [
'type',
'subtype',
'suffix'
]));
});
if (model) {
assert(
!model.forClient || typeof model.forClient === 'function',
`Request body model forClient handler must be a function, not ${typeof model.forClient}`
);
assert(
!model.fromClient || typeof model.fromClient === 'function',
`Request body model fromClient handler must be a function, not ${typeof model.fromClient}`
);
if (!model.fromClient && typeof model.toClient === 'function') {
console.log(
`Found unexpected "toClient" method on request body model.`
+ ' Did you mean "forClient"?'
);
}
}
this._bodyParam = {
model: model,
multiple: multiple,
contentTypes: contentTypes,
description: description
};
}
return this;
}
response(status, type, description) {
response(status, model, mimes, description) {
let statusCode = Number(status);
if (!statusCode || Number.isNaN(statusCode)) {
description = type;
type = status;
description = mimes;
mimes = model;
model = status;
statusCode = 200;
}
if (type === null) {
if (model === null) {
if (statusCode === 200) {
this._responses.remove(200);
statusCode = 204;
}
this._responses.set(statusCode, {});
} else {
let contentType = 'application/json; charset=utf-8';
if (type === 'binary') {
contentType = 'application/octet-stream';
} else if (typeof type !== 'object') {
contentType = mimeTypes.contentType(type) || type;
let multiple = false;
if (
typeof model === 'string'
|| (Array.isArray(model) && typeof model[0] === 'string')
) {
description = mimes;
mimes = model;
model = undefined;
}
const parsed = mediaTyper.parse(contentType);
this._responses.set(statusCode, {
type: type,
description: description,
contentType: {
header: contentType,
parsed: parsed,
mime: mediaTyper.format({
type: parsed.type,
subtype: parsed.subtype,
suffix: parsed.suffix
})
if (!Array.isArray(mimes)) {
mimes = mimes ? [mimes] : [];
}
if (Array.isArray(model)) {
if (model.length !== 1) {
throw new Error(
'Model must be a model or schema'
+ ' or an array containing exactly one model or schema.'
+ ' If you are trying to use multiple schemas'
+ ' try using joi.alternatives.'
);
}
model = model[0];
multiple = true;
}
if (!mimes.length && model) {
mimes.push(MIME_JSON);
}
const contentTypes = mimes.map(function (mime) {
if (mime === 'binary') {
mime = MIME_BINARY;
}
const contentType = mimeTypes.contentType(mime) || mime;
const parsed = mediaTyper.parse(contentType);
return mediaTyper.format(_.pick(parsed, [
'type',
'subtype',
'suffix'
]));
});
if (model) {
assert(
!model.forClient || typeof model.forClient === 'function',
`Response body model forClient handler at ${statusCode} must be a function, not ${typeof model.forClient}`
);
assert(
!model.fromClient || typeof model.fromClient === 'function',
`Response body model fromClient handler at ${statusCode} must be a function, not ${typeof model.fromClient}`
);
if (!model.fromClient && typeof model.toClient === 'function') {
console.log(
`Found unexpected "toClient" method on response body model at ${statusCode}.`
+ ' Did you mean "forClient"?'
);
}
}
this._responses.set(statusCode, {
multiple: multiple,
model: model,
contentTypes: contentTypes,
description: description
});
}
return this;
}
error(status, description) {
return this.response(status, undefined, description);
return this.response(
status,
DEFAULT_ERROR_SCHEMA,
null,
description
);
}
summary(text) {
@ -234,14 +335,19 @@ module.exports = exports = class SwaggerContext {
}
if (this._bodyParam) {
operation.consumes = (
this._bodyParam.contentType
? [this._bodyParam.contentType.mime]
this._bodyParam.contentTypes
? this._bodyParam.contentTypes.slice()
: []
);
}
for (const response of this._responses.values()) {
if (operation.produces.indexOf(response.contentType.mime) === -1) {
operation.produces.push(response.contentType.mime);
if (!response.contentTypes) {
continue;
}
for (const contentType of response.contentTypes) {
if (operation.produces.indexOf(contentType) === -1) {
operation.produces.push(contentType);
}
}
}
if (operation.produces.indexOf('application/json') === -1) {
@ -253,8 +359,8 @@ module.exports = exports = class SwaggerContext {
const name = param[0];
const def = param[1];
const parameter = (
def.type && def.type.isJoi
? swaggerifyParam(def.type)
def.schema
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
: {type: 'string'}
);
parameter.name = name;
@ -270,8 +376,8 @@ module.exports = exports = class SwaggerContext {
const name = param[0];
const def = param[1];
const parameter = (
def.type && def.type.isJoi
? swaggerifyParam(def.type)
def.schema
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
: {type: 'string'}
);
parameter.name = name;
@ -286,8 +392,8 @@ module.exports = exports = class SwaggerContext {
const name = param[0];
const def = param[1];
const parameter = (
def.type && def.type.isJoi
? swaggerifyParam(def.type)
def.schema
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
: {type: 'string'}
);
parameter.name = name;
@ -298,17 +404,16 @@ module.exports = exports = class SwaggerContext {
operation.parameters.push(parameter);
}
if (this._bodyParam && this._bodyParam.contentType) {
// TODO handle multipart and form-urlencoded
if (this._bodyParam && this._bodyParam.contentTypes) {
const def = this._bodyParam;
const schema = (
def.type
? (def.type.schema || def.type)
def.model
? (def.model.isJoi ? def.model : def.model.schema)
: null
);
const parameter = (
schema && schema.isJoi
? swaggerifyBody(schema)
schema
? swaggerifyBody(schema, def.multiple)
: {schema: {type: 'string'}}
);
parameter.name = 'body';
@ -330,19 +435,19 @@ module.exports = exports = class SwaggerContext {
const code = entry[0];
const def = entry[1];
const schema = (
def.type
? (def.type.schema || def.type)
def.model
? (def.model.isJoi ? def.model : def.model.schema)
: null
);
const response = {};
if (def.contentType) {
if (def.contentTypes) {
response.schema = (
schema && schema.isJoi
? joi2schema(schema)
schema
? joi2schema(schema.isJoi ? schema : joi.object(schema), def.multiple)
: {type: 'string'}
);
}
if (schema && schema.isJoi && schema._description) {
if (schema && schema._description) {
response.description = schema._description;
}
if (def.description) {
@ -355,6 +460,7 @@ module.exports = exports = class SwaggerContext {
}
};
exports.DEFAULT_BODY_SCHEMA = DEFAULT_BODY_SCHEMA;
@ -391,6 +497,7 @@ function swaggerifyType(joi) {
}
}
function swaggerifyParam(joi) {
const param = {
required: joi._presence === 'required',
@ -419,10 +526,19 @@ function swaggerifyParam(joi) {
return param;
}
function swaggerifyBody(joi) {
function swaggerifyBody(joi, multiple) {
return {
required: joi._presence === 'required',
description: joi._description,
schema: joi2schema(joi)
schema: joi2schema(joi, multiple)
};
}
function joi2schema(schema, multiple) {
if (multiple) {
schema = joi.array().items(schema);
}
return joiToJsonSchema(schema);
}

View File

@ -205,12 +205,22 @@ function dispatch(route, req, res) {
let pathParams = {};
let queryParams = _.clone(req.queryParams);
let responses = res._responses;
for (const item of route) {
item._responses = union(responses, item._responses);
responses = item._responses;
}
function next(err) {
if (err) {
throw err;
}
const item = route.shift();
if (!item) {
return;
}
const context = (
item.router
? (item.router.router || item.router)
@ -221,7 +231,7 @@ function dispatch(route, req, res) {
try {
req.body = validation.validateRequestBody(
context._bodyParam,
req.body
req
);
} catch (e) {
throw httpError(415, e.message);
@ -239,31 +249,35 @@ function dispatch(route, req, res) {
}
}
if (item.router) {
pathParams = _.extend(pathParams, item.pathParams);
queryParams = _.extend(queryParams, item.queryParams);
next();
return;
}
let tempPathParams = req.pathParams;
let tempQueryParams = req.queryParams;
let tempSuffix = req.suffix;
let tempPath = req.path;
let tempResponses = res._responses;
req.suffix = item.suffix.join('/');
req.path = '/' + item.path.join('/');
res._responses = item._responses;
if (item.endpoint) {
req.pathParams = _.extend(pathParams, item.pathParams);
req.queryParams = _.extend(queryParams, item.queryParams);
item.endpoint._handler(req, res);
} else if (item.middleware) {
if (item.endpoint || item.router) {
pathParams = _.extend(pathParams, item.pathParams);
queryParams = _.extend(queryParams, item.queryParams);
req.pathParams = pathParams;
req.queryParams = queryParams;
} else {
req.pathParams = _.extend(_.clone(pathParams), item.pathParams);
req.queryParams = _.extend(_.clone(queryParams), item.queryParams);
item.middleware._handler(req, res, next);
}
if (!context._handler) {
next();
} else if (item.endpoint) {
context._handler(req, res);
} else {
context._handler(req, res, _.once(next));
}
res._responses = tempResponses;
req.path = tempPath;
req.suffix = tempSuffix;
req.queryParams = tempQueryParams;
@ -271,6 +285,15 @@ function dispatch(route, req, res) {
}
next();
if (res.body && typeof res.body !== 'string' && !(res.body instanceof Buffer)) {
require('console').warn(`Coercing response body to string for ${req.method} ${req.originalUrl}`);
res.body = String(res.body);
}
if (!res.statusCode) {
res.statusCode = res.body ? 200 : 204;
}
}

View File

@ -26,6 +26,13 @@
/// @author Copyright 2016, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const assert = require('assert');
const typeIs = require('type-is');
const mediaTyper = require('media-typer');
const requestParts = require('internal').requestParts;
exports.validateParams = function validateParams(typeDefs, rawParams) {
const params = {};
for (let entry of typeDefs) {
@ -42,47 +49,75 @@ exports.validateParams = function validateParams(typeDefs, rawParams) {
return params;
};
exports.validateRequestBody = function validateRequestBody(def, rawBody) {
if (def.type === null) {
if (rawBody && rawBody.length) {
throw new Error('Unexpected request body');
}
exports.validateRequestBody = function validateRequestBody(def, req) {
let body = req.body;
if (!def.contentTypes) {
assert(!body, 'Unexpected request body');
return null;
}
const isJson = def.mime.subtype === 'json' || def.mime.suffix === 'json';
const charset = def.mime.parameters.charset;
let indicatedType = req.get('content-type');
let actualType;
if (
(def.mime.type !== 'text' && !isJson) ||
(charset && charset.toLowerCase() !== 'utf-8')
) {
return rawBody;
if (indicatedType) {
for (const candidate of def.contentTypes) {
if (typeIs.is(indicatedType, candidate)) {
actualType = candidate;
break;
}
}
}
const textBody = rawBody.toString('utf-8');
if (!isJson) {
return textBody;
if (!actualType) {
actualType = def.contentTypes[0];
}
const jsonBody = JSON.parse(textBody);
const schema = def.type.schema || def.type;
const parsedType = mediaTyper.parse(actualType);
actualType = mediaTyper.format(_.pick(actualType, ['type', 'subtype', 'suffix']));
if (!schema.isJoi) {
return jsonBody;
if (parsedType.type === 'multipart') {
body = requestParts(req._raw);
}
const result = schema.validate(jsonBody);
if (result.error) {
result.error.message = result.error.message.replace(/^"value"/, '"request body"');
throw result.error;
let handler;
for (const entry of req.context.service.types.entries()) {
const key = entry[0];
const value = entry[1];
let match;
if (key instanceof RegExp) {
match = actualType.test(key);
} else if (typeof key === 'function') {
match = key(actualType);
} else {
match = typeIs.is(key, actualType);
}
if (match && value.fromClient) {
handler = value;
break;
}
}
if (schema === def.type || typeof def.type.fromClient !== 'function') {
return result.value;
if (handler) {
body = handler.fromClient(body, req, parsedType);
}
return def.type.fromClient(result.value);
const schema = def.model && (def.model.schema || def.model);
if (schema.isJoi) {
const result = schema.validate(body);
if (result.error) {
result.error.message = result.error.message.replace(/^"value"/, '"request body"');
throw result.error;
}
body = result.value;
}
if (def.model && def.model.fromClient) {
body = def.model.fromClient(body);
}
return body;
};

View File

@ -138,18 +138,12 @@ exports.routeApp = function (service, throwOnErrors) {
};
}
Object.keys(service.requireCache).forEach(function (key) {
// Clear the module cache to force re-evaluation
delete service.requireCache[key];
});
service.main.exports = {};
service._reset();
let error = null;
if (service.legacy) {
error = routeLegacyService(service, throwOnErrors);
} else {
service.router = createRouter();
service.routes = {
name: `foxx("${service.mount}")`,
routes: []
@ -182,7 +176,7 @@ exports.routeApp = function (service, throwOnErrors) {
service.main.exports = service.run(service.manifest.main);
// TODO mount routes
} catch (e) {
console.errorLines(`Cannot execute Foxx service: ${e.stack}`);
console.errorLines(`Cannot execute Foxx service at ${service.mount}: ${e.stack}`);
error = e;
if (throwOnErrors) {
throw e;

View File

@ -1,7 +1,7 @@
'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief FoxxService and FoxxContext types
/// @brief FoxxService type
///
/// @file
///
@ -24,7 +24,7 @@
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Alan Plum
/// @author Copyright 2015, triAGENS GmbH, Cologne, Germany
/// @author Copyright 2015-2016, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
@ -35,9 +35,11 @@ const Module = require('module');
const semver = require('semver');
const path = require('path');
const fs = require('fs');
const defaultTypes = require('@arangodb/foxx/types');
const FoxxContext = require('@arangodb/foxx/context');
const parameterTypes = require('@arangodb/foxx/manager-utils').parameterTypes;
const getReadableName = require('@arangodb/foxx/manager-utils').getReadableName;
const createRouter = require('@arangodb/foxx/router');
const Router = require('@arangodb/foxx/router/router');
const Tree = require('@arangodb/foxx/router/tree');
const $_MODULE_ROOT = Symbol.for('@arangodb/module.root');
@ -47,119 +49,8 @@ const APP_PATH = internal.appPath ? path.resolve(internal.appPath) : undefined;
const STARTUP_PATH = internal.startupPath ? path.resolve(internal.startupPath) : undefined;
const DEV_APP_PATH = internal.devAppPath ? path.resolve(internal.devAppPath) : undefined;
class FoxxContext {
constructor(service) {
this.service = service;
this.argv = [];
}
use(path, router) {
this.service.router.use(path, router);
}
fileName(filename) {
return fs.safeJoin(this.basePath, filename);
}
file(filename, encoding) {
return fs.readFileSync(this.fileName(filename), encoding);
}
path(name) {
return path.join(this.basePath, name);
}
collectionName(name) {
let fqn = (
this.collectionPrefix
+ name.replace(/[^a-z0-9]/ig, '_').replace(/(^_+|_+$)/g, '').substr(0, 64)
);
if (!fqn.length) {
throw new Error(`Cannot derive collection name from "${name}"`);
}
return fqn;
}
collection(name) {
return internal.db._collection(this.collectionName(name));
}
get basePath() {
return this.service.basePath;
}
get baseUrl() {
return `/_db/${encodeURIComponent(internal.db._name())}/${this.service.mount.slice(1)}`;
}
get collectionPrefix() {
return this.service.collectionPrefix;
}
get mount() {
return this.service.mount;
}
get name() {
return this.service.name;
}
get version() {
return this.service.version;
}
get manifest() {
return this.service.manifest;
}
get isDevelopment() {
return this.service.isDevelopment;
}
get isProduction() {
return !this.isDevelopment;
}
get options() {
return this.service.options;
}
get configuration() {
return this.service.configuration;
}
get dependencies() {
return this.service.dependencies;
}
}
function createConfiguration(definitions) {
const config = {};
Object.keys(definitions).forEach(function (name) {
const def = definitions[name];
if (def.default !== undefined) {
config[name] = def.default;
}
});
return config;
}
function createDependencies(definitions, options) {
const deps = {};
Object.keys(definitions).forEach(function (name) {
Object.defineProperty(deps, name, {
configurable: true,
enumerable: true,
get() {
const mount = options[name];
return mount ? require('@arangodb/foxx').getExports(mount) : null;
}
});
});
return deps;
}
class FoxxService {
module.exports = class FoxxService {
constructor(data) {
assert(data, 'no arguments');
assert(data.mount, 'mount path required');
@ -214,26 +105,15 @@ class FoxxService {
this.thumbnail = null;
}
this.requireCache = {};
const lib = this.manifest.lib || '.';
const moduleRoot = path.resolve(this.root, this.path, lib);
const foxxConsole = require('@arangodb/foxx/console')(this.mount);
this.main = new Module(`foxx:${data.mount}`);
this.main.filename = path.resolve(moduleRoot, '.foxx');
this.main[$_MODULE_ROOT] = moduleRoot;
this.main[$_MODULE_CONTEXT].console = foxxConsole;
this.main.require.cache = this.requireCache;
this.main.context = new FoxxContext(this);
this.router = createRouter();
let range = this.manifest.engines && this.manifest.engines.arangodb;
const range = this.manifest.engines && this.manifest.engines.arangodb;
this.legacy = range ? semver.gtr('3.0.0', range) : false;
if (this.legacy) {
console.debug(
`Running ${data.mount} in 2.x compatibility mode (requested version ${range} pre-dates 3.0.0)`
`Running ${this.mount} in 2.x compatibility mode (requested version ${range} pre-dates 3.0.0)`
);
this.main.context.foxxFilename = this.main.context.fileName;
}
this._reset();
}
applyConfiguration(config) {
@ -341,8 +221,8 @@ class FoxxService {
}
applyDependencies(deps) {
var definitions = this.manifest.dependencies;
var warnings = [];
const definitions = this.manifest.dependencies;
const warnings = [];
_.each(deps, function (mount, name) {
const dfn = definitions[name];
@ -400,11 +280,11 @@ class FoxxService {
}
getConfiguration(simple) {
var config = {};
var definitions = this.manifest.configuration;
var options = this.options.configuration;
const config = {};
const definitions = this.manifest.configuration;
const options = this.options.configuration;
_.each(definitions, function (dfn, name) {
var value = options[name] === undefined ? dfn.default : options[name];
const value = options[name] === undefined ? dfn.default : options[name];
config[name] = simple ? value : _.extend({}, dfn, {
title: getReadableName(name),
current: value
@ -414,9 +294,9 @@ class FoxxService {
}
getDependencies(simple) {
var deps = {};
var definitions = this.manifest.dependencies;
var options = this.options.dependencies;
const deps = {};
const definitions = this.manifest.dependencies;
const options = this.options.dependencies;
_.each(definitions, function (dfn, name) {
deps[name] = simple ? options[name] : {
definition: dfn,
@ -428,8 +308,8 @@ class FoxxService {
}
needsConfiguration() {
var config = this.getConfiguration();
var deps = this.getDependencies();
const config = this.getConfiguration();
const deps = this.getDependencies();
return _.any(config, function (cfg) {
return cfg.current === undefined && cfg.required !== false;
}) || _.any(deps, function (dep) {
@ -441,7 +321,7 @@ class FoxxService {
options = options || {};
filename = path.resolve(this.main[$_MODULE_CONTEXT].__dirname, filename);
var module = new Module(filename, this.main);
const module = new Module(filename, this.main);
module[$_MODULE_CONTEXT].console = this.main[$_MODULE_CONTEXT].console;
module.context = _.extend(
new FoxxContext(this),
@ -463,6 +343,26 @@ class FoxxService {
return module.exports;
}
_reset() {
this.requireCache = {};
const lib = this.manifest.lib || '.';
const moduleRoot = path.resolve(this.root, this.path, lib);
const foxxConsole = require('@arangodb/foxx/console')(this.mount);
this.main = new Module(`foxx:${this.mount}`);
this.main.filename = path.resolve(moduleRoot, '.foxx');
this.main[$_MODULE_ROOT] = moduleRoot;
this.main[$_MODULE_CONTEXT].console = foxxConsole;
this.main.require.cache = this.requireCache;
this.main.context = new FoxxContext(this);
this.router = new Router();
this.types = new Map(defaultTypes);
if (this.legacy) {
this.main.context.foxxFilename = this.main.context.fileName;
}
}
get exports() {
return this.main.exports;
}
@ -517,6 +417,32 @@ class FoxxService {
path.join(DEV_APP_PATH, 'databases', internal.db._name())
) : undefined;
}
};
function createConfiguration(definitions) {
const config = {};
Object.keys(definitions).forEach(function (name) {
const def = definitions[name];
if (def.default !== undefined) {
config[name] = def.default;
}
});
return config;
}
module.exports = FoxxService;
function createDependencies(definitions, options) {
const deps = {};
Object.keys(definitions).forEach(function (name) {
Object.defineProperty(deps, name, {
configurable: true,
enumerable: true,
get() {
const mount = options[name];
return mount ? require('@arangodb/foxx').getExports(mount) : null;
}
});
});
return deps;
}

View File

@ -0,0 +1,137 @@
'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief Foxx default type handlers
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2016 triagens 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 triAGENS GmbH, Cologne, Germany
///
/// @author Alan Plum
/// @author Copyright 2016, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
const assert = require('assert');
const mimeTypes = require('mime-types');
const querystring = require('querystring');
const contentDisposition = require('content-disposition');
const DEFAULT_CHARSET = 'utf-8';
module.exports = new Map([
[mimeTypes.lookup('text'), {
fromClient(body, req, type) {
body = stringifyBuffer(body, type.parameters.charset);
return body;
},
forClient(body) {
return {
data: String(body),
headers: {
'content-type': mimeTypes.contentType('text')
}
};
}
}],
[mimeTypes.lookup('json'), {
fromClient(body, req, type) {
body = stringifyBuffer(body, type.parameters.charset);
return JSON.parse(body);
},
forClient(body) {
return {
data: JSON.stringify(body),
headers: {
'content-type': mimeTypes.contentType('json')
}
};
}
}],
['application/x-www-form-urlencoded', {
fromClient(body, req, type) {
body = stringifyBuffer(body, type.parameters.charset);
return querystring.parse(body);
},
forClient(body) {
return {
data: querystring.stringify(body),
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
};
}
}]/*,
['multipart/form-data', {
fromClient(body) {
assert(
Array.isArray(body) && body.every(function (part) {
return (
part && typeof part === 'object'
&& part.headers && typeof part.headers === 'object'
&& part.data instanceof Buffer
);
}),
`Expecting a multipart array, not ${body ? typeof body : String(body)}`
);
const form = new Map();
for (const part of body) {
const dispositionHeader = part.headers && part.headers['content-disposition'];
if (!dispositionHeader) {
continue;
}
const disposition = contentDisposition.parse(dispositionHeader);
const name = disposition.parameters.name;
if (disposition.type !== 'form-data' || !name) {
continue;
}
let value = part.data;
// TODO parse value according to headers?
if (!form.has(name)) {
form.set(name, value);
} else if (!Array.isArray(form.get(name))) {
form.set(name, [form.get(name), value]);
} else {
form.get(name).push(value);
}
}
return form;
}
}]*/
]);
function stringifyBuffer(buf, charset) {
if (!(buf instanceof Buffer)) {
return buf;
}
charset = charset || DEFAULT_CHARSET;
try {
return buf.toString(charset);
} catch (e) {
if (charset === DEFAULT_CHARSET) {
throw e;
}
console.warn(
`Unable to parse buffer with charset "${charset}",`
+ ` falling back to default "${DEFAULT_CHARSET}"`
);
return buf.toString(DEFAULT_CHARSET);
}
}

View File

@ -434,7 +434,7 @@ describe('Tree#buildSwaggerPaths', function () {
const mimeType = 'banana';
const router = new Router();
const route = router.post('/data', function () {});
route._bodyParam.contentType.mime = mimeType;
route._bodyParam.contentTypes[0] = mimeType;
const tree = new Tree({}, router);
const docs = tree.buildSwaggerPaths();
expect(docs).to.have.a.property('/data')
@ -476,7 +476,7 @@ describe('Tree#buildSwaggerPaths', function () {
const mimeType = 'banana';
const router = new Router();
const route = router.get('/body', function () {});
route._responses.set(200, {contentType: {mime: mimeType}});
route._responses.set(200, {contentTypes: [mimeType]});
const tree = new Tree({}, router);
const docs = tree.buildSwaggerPaths();
expect(docs).to.have.a.property('/body')
@ -488,8 +488,8 @@ describe('Tree#buildSwaggerPaths', function () {
const mimeType = 'banana';
const router = new Router();
const route = router.get('/body', function () {});
route._responses.set(200, {contentType: {mime: mimeType}});
route._responses.set(400, {contentType: {mime: mimeType}});
route._responses.set(200, {contentTypes: [mimeType]});
route._responses.set(400, {contentTypes: [mimeType]});
const tree = new Tree({}, router);
const docs = tree.buildSwaggerPaths();
expect(docs).to.have.a.property('/body')
@ -507,5 +507,6 @@ describe('Tree#buildSwaggerPaths', function () {
it.skip('sets the default response');
it.skip('sets the response bodies');
it.skip('omits the response body if body is explicitly disabled');
it.skip('sets the form parameters if route consumes form type but not json');
});