mirror of https://gitee.com/bigwinds/arangodb
385 lines
9.1 KiB
JavaScript
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;
|
|
}
|
|
};
|