mirror of https://gitee.com/bigwinds/arangodb
Implement request/response body handling
This commit is contained in:
parent
a5ffbcf736
commit
8d2cc4041d
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue