1
0
Fork 0
arangodb/js/server/modules/@arangodb/foxx/router/response.js

385 lines
9.1 KiB
JavaScript

'use strict';
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2016 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 Alan Plum
////////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const fs = require('fs');
const vary = require('vary');
const httperr = require('http-errors');
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 actions = require('@arangodb/actions');
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, context) {
this._raw = res;
this._responses = new Map();
this.context = context;
}
// Node compat
get headers() {
if (!this._raw.headers) {
this._raw.headers = {};
}
return this._raw.headers;
}
get statusCode() {
return this._raw.responseCode;
}
set statusCode(value) {
this._raw.responseCode = value;
}
get body() {
return this._raw.body;
}
set body(data) {
if (data === null || data === undefined) {
delete this._raw.body;
return;
}
if (typeof data === 'string' || data instanceof Buffer) {
this._raw.body = data;
} else if (typeof data === 'object') {
if (this.context.isDevelopment) {
this._raw.body = JSON.stringify(data, null, 2);
} else {
this._raw.body = JSON.stringify(data);
}
} else {
this._raw.body = String(data);
}
}
getHeader(name) {
name = name.toLowerCase();
if (name === 'content-type') {
return this._raw.contentType;
}
return this.headers[name];
}
removeHeader(name) {
name = name.toLowerCase();
if (name === 'content-type') {
delete this._raw.contentType;
}
delete this.headers[name];
return this;
}
setHeader(name, value) {
if (!name) {
return this;
}
name = name.toLowerCase();
if (name === 'content-type') {
this._raw.contentType = value;
} else {
this.headers[name] = value;
}
return this;
}
write(data) {
const bodyIsBuffer = this._raw.body instanceof Buffer;
const dataIsBuffer = data instanceof Buffer;
if (data === null || data === undefined) {
return this;
}
if (!dataIsBuffer) {
if (typeof data === 'object') {
if (this.context.isDevelopment) {
data = JSON.stringify(data, null, 2);
} else {
data = JSON.stringify(data);
}
} else {
data = String(data);
}
}
if (!this._raw.body) {
this._raw.body = data;
} else if (bodyIsBuffer || dataIsBuffer) {
this._raw.body = Buffer.concat([
bodyIsBuffer ? this._raw.body : new Buffer(this._raw.body),
dataIsBuffer ? data : new Buffer(data)
]);
} else {
this._raw.body += data;
}
return this;
}
// Express compat
attachment(filename) {
this.headers['content-disposition'] = contentDisposition(filename);
if (filename && !this._raw.contentType) {
this._raw.contentType = mimeTypes.lookup(filename) || MIME_BINARY;
}
return this;
}
download(path, filename) {
this.attachment(filename || path);
this.sendFile(path);
return this;
}
json(value) {
if (!this._raw.contentType) {
this._raw.contentType = MIME_JSON;
}
if (this.context.isDevelopment) {
this._raw.body = JSON.stringify(value, null, 2);
} else {
this._raw.body = JSON.stringify(value);
}
return this;
}
redirect(status, path) {
if (!path) {
path = status;
status = undefined;
}
if (status === 'permanent') {
status = 301;
}
if (status || !this.statusCode) {
this.statusCode = status || 302;
}
this.setHeader('location', path);
return this;
}
sendFile(filename, opts) {
if (typeof opts === 'boolean') {
opts = {lastModified: opts};
}
if (!opts) {
opts = {};
}
this._raw.body = fs.readBuffer(filename);
if (opts.lastModified || (
opts.lastModified !== false && !this.headers['last-modified']
)) {
const lastModified = new Date(fs.mtime(filename) * 1000);
this.headers['last-modified'] = lastModified.toUTCString();
}
if (!this._raw.contentType) {
this._raw.contentType = mimeTypes.lookup(filename) || MIME_BINARY;
}
return this;
}
sendStatus(status) {
if (typeof status === 'string') {
status = statuses(status);
}
const message = String(statuses[status] || status);
this.statusCode = status;
this.body = message;
return this;
}
set(name, value) {
if (name && typeof name === 'object') {
_.each(name, (v, k) => this.set(k, v));
} else {
this.setHeader(name, value);
}
return this;
}
status(statusCode) {
if (typeof statusCode === 'string') {
statusCode = statuses(statusCode);
}
this.statusCode = statusCode;
return this;
}
vary(value) {
const header = this.getHeader('vary');
if (header) {
value = vary.append(header, value);
}
this.setHeader('vary', value);
return this;
}
// idiosyncratic
cookie(name, value, opts) {
if (!opts) {
opts = {};
} else if (typeof opts === 'string') {
opts = {secret: opts};
} else if (typeof opts === 'number') {
opts = {ttl: opts};
}
const ttl = (
(typeof opts.ttl === 'number' && opts.ttl !== Infinity)
? opts.ttl
: undefined
);
actions.addCookie(
this._raw,
name,
value,
ttl,
opts.path,
opts.domain,
opts.secure,
opts.httpOnly
);
if (opts.secret) {
const signature = crypto.hmac(opts.secret, value, opts.algorithm);
actions.addCookie(
this._raw,
`${name}.sig`,
signature,
ttl,
opts.path,
opts.domain,
opts.secure,
opts.httpOnly
);
}
return this;
}
throw(status, reason, args) {
if (typeof status === 'string') {
status = statuses(status);
}
if (reason instanceof Error) {
const err = reason;
reason = err.message;
args = Object.assign({
cause: err,
errorNum: err.errorNum
}, args);
}
if (reason && typeof reason === 'object') {
args = reason;
reason = undefined;
}
throw Object.assign(
httperr(status, reason),
{statusCode: status, status},
args
);
}
send(body, type) {
if (body && body.isArangoResultSet) {
body = body.toArray();
}
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) {
if (response.multiple && Array.isArray(body)) {
body = body.map((item) => response.model.forClient(item));
} else {
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.forClient) {
contentType = key;
handler = value;
break;
}
}
if (handler) {
const result = handler.forClient(body, this, mediaTyper.parse(contentType));
if (result.headers || result.data) {
contentType = result.headers['content-type'] || contentType;
this.set(result.headers);
body = result.data;
} else {
body = result;
}
}
this._raw.body = body;
this._raw.contentType = contentType || this._raw.contentType;
return this;
}
};