Ghost/core/server/api/shared/http.js
naz cbdc91ce48
Added Location header to API's POST request responses (#12186)
refs #2635

- Adds 'Location' header to endpoints which create new resources and have corresponding `GET` endpoint as speced in JSON API - https://jsonapi.org/format/#crud-creating-responses-201. Specifically:
    /posts/
    /pages/
    /integrations/
    /tags/
    /members/
    /labels/
    /notifications/
    /invites/

- Adding the header should allow for better resource discoverability and improved logging readability
- Added `url` property to the frame constructor. Data in `url` should give enough information  to later build up the `Location` header URL for created resource.
- Added Location header to headers handler. The Location value is built up from a combination of request URL and the id that is present in the response for the resource. The header is automatically added to requests coming to `add` controller methods which return `id` property in the frame result
- Excluded Webhooks API  as there is no "GET" endpoint available to fetch the resource
2020-09-14 22:33:37 +12:00

108 lines
3.4 KiB
JavaScript

const url = require('url');
const debug = require('ghost-ignition').debug('api:shared:http');
const shared = require('../shared');
const models = require('../../models');
/**
* @description HTTP wrapper.
*
* This wrapper is used in the routes definition (see web/).
* The wrapper receives the express request, prepares the frame and forwards the request to the pipeline.
*
* @param {Function} apiImpl - Pipeline wrapper, which executes the target ctrl function.
* @return {Function}
*/
const http = (apiImpl) => {
return (req, res, next) => {
debug(`External API request to ${req.url}`);
let apiKey = null;
let integration = null;
let user = null;
if (req.api_key) {
apiKey = {
id: req.api_key.get('id'),
type: req.api_key.get('type')
};
integration = {
id: req.api_key.get('integration_id')
};
}
// NOTE: "external user" is only used in the subscriber app. External user is ID "0".
if ((req.user && req.user.id) || (req.user && models.User.isExternalUser(req.user.id))) {
user = req.user.id;
}
const frame = new shared.Frame({
body: req.body,
file: req.file,
files: req.files,
query: req.query,
params: req.params,
user: req.user,
session: req.session,
url: {
host: req.vhost ? req.vhost.host : req.get('host'),
pathname: url.parse(req.originalUrl || req.url).pathname,
secure: req.secure
},
context: {
api_key: apiKey,
user: user,
integration: integration,
member: (req.member || null)
}
});
frame.configure({
options: apiImpl.options,
data: apiImpl.data
});
apiImpl(frame)
.then((result) => {
debug(`External API request to ${frame.docName}.${frame.method}`);
return shared.headers.get(result, apiImpl.headers, frame)
.then(headers => ({result, headers}));
})
.then(({result, headers}) => {
// CASE: api ctrl wants to handle the express response (e.g. streams)
if (typeof result === 'function') {
debug('ctrl function call');
return result(req, res, next);
}
let statusCode = 200;
if (typeof apiImpl.statusCode === 'function') {
statusCode = apiImpl.statusCode(result);
} else if (apiImpl.statusCode) {
statusCode = apiImpl.statusCode;
}
res.status(statusCode);
// CASE: generate headers based on the api ctrl configuration
res.set(headers);
if (apiImpl.response && apiImpl.response.format === 'plain') {
debug('plain text response');
return res.send(result);
}
debug('json response');
res.json(result || {});
})
.catch((err) => {
req.frameOptions = {
docName: frame.docName,
method: frame.method
};
next(err);
});
};
};
module.exports = http;