mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Added comments for Ghost API
no issue - jsdoc - added more information & context
This commit is contained in:
parent
79345f9030
commit
a31ed7c71d
11 changed files with 241 additions and 23 deletions
|
@ -2,28 +2,28 @@
|
|||
|
||||
Ghost supports multiple API versions.
|
||||
Each version lives in a separate folder e.g. api/v0.1, api/v2.
|
||||
Next to the API folders there is a shared folder, which the API versions use.
|
||||
Next to the API folders there is a shared folder, which contains shared code, which all API versions use.
|
||||
|
||||
**NOTE: v0.1 is deprecated and we won't touch this folder at all. The v0.1 folder
|
||||
**NOTE: v0.1 is deprecated and we won't touch the shared folder at all. The v0.1 folder
|
||||
contains the API layer which we have used since Ghost was born.**
|
||||
|
||||
## Stages
|
||||
|
||||
Each request goes through the following stages:
|
||||
|
||||
- validation
|
||||
- input validation
|
||||
- input serialisation
|
||||
- permissions
|
||||
- query
|
||||
- output serialisation
|
||||
|
||||
The framework we are building pipes a request through these stages depending on the API controller implementation.
|
||||
The framework we are building pipes a request through these stages in respect of the API controller configuration.
|
||||
|
||||
|
||||
## Frame
|
||||
|
||||
Is a class, which holds all the information for API processing. We pass this instance per reference.
|
||||
The target function can modify the original instance. No need to return the class instance.
|
||||
Is a class, which holds all the information for request processing. We pass this instance by reference.
|
||||
Each function can modify the original instance. No need to return the class instance.
|
||||
|
||||
### Structure
|
||||
|
||||
|
@ -83,7 +83,9 @@ edit: {
|
|||
headers: {
|
||||
cacheInvalidate: true
|
||||
},
|
||||
// Allowed url/query params
|
||||
options: ['include']
|
||||
// Url/query param validation configuration
|
||||
validation: {
|
||||
options: {
|
||||
include: {
|
||||
|
@ -93,6 +95,7 @@ edit: {
|
|||
}
|
||||
},
|
||||
permissions: true,
|
||||
// Returns a model response!
|
||||
query(frame) {
|
||||
return models.Post.edit(frame.data, frame.options);
|
||||
}
|
||||
|
@ -101,6 +104,9 @@ edit: {
|
|||
|
||||
```
|
||||
read: {
|
||||
// Allowed url/query params, which will be remembered inside `frame.data`
|
||||
// This is helpful for READ requests e.g. `model.findOne(frame.data, frame.options)`.
|
||||
// Our model layer requires sending the where clauses as first parameter.
|
||||
data: ['slug']
|
||||
validation: {
|
||||
data: {
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
const debug = require('ghost-ignition').debug('api:shared:frame');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @description The "frame" holds all information of a request.
|
||||
*
|
||||
* Each party can modify the frame by reference.
|
||||
* A request hits a lot of stages in the API implementation and that's why modification by reference was the
|
||||
* easiest to use. We always have access to the original input, we never loose track of it.
|
||||
*/
|
||||
class Frame {
|
||||
constructor(obj = {}) {
|
||||
this.original = obj;
|
||||
|
||||
/**
|
||||
* options: Query params, url params, context and custom options
|
||||
* data: Body or if the ctrl wants query/url params inside body
|
||||
* user: Logged in user
|
||||
* file: Uploaded file
|
||||
* files: Uploaded files
|
||||
* apiType: Content or admin api access
|
||||
*/
|
||||
this.options = {};
|
||||
this.data = {};
|
||||
this.user = {};
|
||||
|
@ -14,7 +29,12 @@ class Frame {
|
|||
}
|
||||
|
||||
/**
|
||||
* If you instantiate a new frame, all the data you pass in, land in `this.original`.
|
||||
* @description Configure the frame.
|
||||
*
|
||||
* If you instantiate a new frame, all the data you pass in, land in `this.original`. This is helpful
|
||||
* for debugging to see what the original input was.
|
||||
*
|
||||
* This function will prepare the incoming data for further processing.
|
||||
* Based on the API ctrl implemented, this fn will pick allowed properties to either options or data.
|
||||
*/
|
||||
configure(apiConfig) {
|
||||
|
|
|
@ -11,6 +11,13 @@ const cacheInvalidate = (result, options = {}) => {
|
|||
};
|
||||
|
||||
const disposition = {
|
||||
/**
|
||||
* @description Generate CSV header.
|
||||
*
|
||||
* @param {Object} result - API response
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
csv(result, options = {}) {
|
||||
let value = options.value;
|
||||
|
||||
|
@ -24,6 +31,13 @@ const disposition = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Generate JSON header.
|
||||
*
|
||||
* @param {Object} result - API response
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
json(result, options = {}) {
|
||||
return {
|
||||
'Content-Disposition': `Attachment; filename="${options.value}"`,
|
||||
|
@ -32,6 +46,13 @@ const disposition = {
|
|||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Generate YAML header.
|
||||
*
|
||||
* @param {Object} result - API response
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
yaml(result, options = {}) {
|
||||
return {
|
||||
'Content-Disposition': `Attachment; filename="${options.value}"`,
|
||||
|
@ -41,8 +62,9 @@ const disposition = {
|
|||
},
|
||||
|
||||
/**
|
||||
* ### Content Disposition Header
|
||||
* create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename'
|
||||
* @description Content Disposition Header
|
||||
*
|
||||
* Create a header that invokes the 'Save As' dialog in the browser when exporting the database to file. The 'filename'
|
||||
* parameter is governed by [RFC6266](http://tools.ietf.org/html/rfc6266#section-4.3).
|
||||
*
|
||||
* For encoding whitespace and non-ISO-8859-1 characters, you MUST use the "filename*=" attribute, NOT "filename=".
|
||||
|
@ -72,6 +94,13 @@ const disposition = {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* @description Get header based on ctrl configuration.
|
||||
*
|
||||
* @param {Object} result - API response
|
||||
* @param {Object} apiConfig
|
||||
* @return {Promise}
|
||||
*/
|
||||
get(result, apiConfig = {}) {
|
||||
let headers = {};
|
||||
|
||||
|
|
|
@ -2,6 +2,15 @@ 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('request');
|
||||
|
@ -20,6 +29,7 @@ const http = (apiImpl) => {
|
|||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,20 @@ const sequence = require('../../lib/promise/sequence');
|
|||
|
||||
const STAGES = {
|
||||
validation: {
|
||||
/**
|
||||
* @description Input validation.
|
||||
*
|
||||
* We call the shared validator which runs the request through:
|
||||
*
|
||||
* 1. Shared serializers
|
||||
* 2. Custom API serializers
|
||||
*
|
||||
* @param {Object} apiUtils - Local utils of target API version.
|
||||
* @param {Object} apiConfig - Docname & Method of ctrl.
|
||||
* @param {Object} apiImpl - Controller configuration.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
input(apiUtils, apiConfig, apiImpl, frame) {
|
||||
debug('stages: validation');
|
||||
const tasks = [];
|
||||
|
@ -30,6 +44,20 @@ const STAGES = {
|
|||
},
|
||||
|
||||
serialisation: {
|
||||
/**
|
||||
* @description Input Serialisation.
|
||||
*
|
||||
* We call the shared serializer which runs the request through:
|
||||
*
|
||||
* 1. Shared serializers
|
||||
* 2. Custom API serializers
|
||||
*
|
||||
* @param {Object} apiUtils - Local utils of target API version.
|
||||
* @param {Object} apiConfig - Docname & Method of ctrl.
|
||||
* @param {Object} apiImpl - Controller configuration.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
input(apiUtils, apiConfig, apiImpl, frame) {
|
||||
debug('stages: input serialisation');
|
||||
return shared.serializers.handle.input(
|
||||
|
@ -38,12 +66,40 @@ const STAGES = {
|
|||
frame
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Output Serialisation.
|
||||
*
|
||||
* We call the shared serializer which runs the request through:
|
||||
*
|
||||
* 1. Shared serializers
|
||||
* 2. Custom API serializers
|
||||
*
|
||||
* @param {Object} apiUtils - Local utils of target API version.
|
||||
* @param {Object} apiConfig - Docname & Method of ctrl.
|
||||
* @param {Object} apiImpl - Controller configuration.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
output(response, apiUtils, apiConfig, apiImpl, frame) {
|
||||
debug('stages: output serialisation');
|
||||
return shared.serializers.handle.output(response, apiConfig, apiUtils.serializers.output, frame);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Permissions stage.
|
||||
*
|
||||
* We call the target API implementation of permissions.
|
||||
* Permissions implementation can change across API versions.
|
||||
* There is no shared implementation right now.
|
||||
*
|
||||
* @param {Object} apiUtils - Local utils of target API version.
|
||||
* @param {Object} apiConfig - Docname & Method of ctrl.
|
||||
* @param {Object} apiImpl - Controller configuration.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
permissions(apiUtils, apiConfig, apiImpl, frame) {
|
||||
debug('stages: permissions');
|
||||
const tasks = [];
|
||||
|
@ -81,6 +137,15 @@ const STAGES = {
|
|||
return sequence(tasks);
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Execute controller & receive model response.
|
||||
*
|
||||
* @param {Object} apiUtils - Local utils of target API version.
|
||||
* @param {Object} apiConfig - Docname & Method of ctrl.
|
||||
* @param {Object} apiImpl - Controller configuration.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
query(apiUtils, apiConfig, apiImpl, frame) {
|
||||
debug('stages: query');
|
||||
|
||||
|
@ -92,6 +157,25 @@ const STAGES = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description The pipeline runs the request through all stages (validation, serialisation, permissions).
|
||||
*
|
||||
* The target API version calls the pipeline and wraps the actual ctrl implementation to be able to
|
||||
* run the request through various stages before hitting the controller.
|
||||
*
|
||||
* The stages are executed in the following order:
|
||||
*
|
||||
* 1. Input validation - General & schema validation
|
||||
* 2. Input serialisation - Modification of incoming data e.g. force filters, auto includes, url transformation etc.
|
||||
* 3. Permissions - Runs after validation & serialisation because the body structure must be valid (see unsafeAttrs)
|
||||
* 4. Controller - Execute the controller implementation & receive model response.
|
||||
* 5. Output Serialisation - Output formatting, Deprecations, Extra attributes etc...
|
||||
*
|
||||
* @param {Function} apiController
|
||||
* @param {Object} apiUtils - Local utils (validation & serialisation) from target API version
|
||||
* @param {String} apiType - Content or Admin API access
|
||||
* @return {Function}
|
||||
*/
|
||||
const pipeline = (apiController, apiUtils, apiType) => {
|
||||
const keys = Object.keys(apiController);
|
||||
|
||||
|
|
|
@ -4,10 +4,16 @@ const sequence = require('../../../lib/promise/sequence');
|
|||
const common = require('../../../lib/common');
|
||||
|
||||
/**
|
||||
* The shared serialization handler runs the request through all the serialization steps.
|
||||
* @description Shared input serialization handler.
|
||||
*
|
||||
* 1. shared serialization
|
||||
* 2. api serialization
|
||||
* The shared input handler runs the request through all the validation steps.
|
||||
*
|
||||
* 1. Shared serialization
|
||||
* 2. API serialization
|
||||
*
|
||||
* @param {Object} apiConfig - Docname + method of the ctrl
|
||||
* @param {Object} apiSerializers - Target API serializers
|
||||
* @param {Object} frame
|
||||
*/
|
||||
module.exports.input = (apiConfig, apiSerializers, frame) => {
|
||||
debug('input');
|
||||
|
@ -61,7 +67,20 @@ module.exports.input = (apiConfig, apiSerializers, frame) => {
|
|||
return sequence(tasks);
|
||||
};
|
||||
|
||||
module.exports.output = (response = {}, apiConfig, apiSerializers, options) => {
|
||||
/**
|
||||
* @description Shared output serialization handler.
|
||||
*
|
||||
* The shared output handler runs the request through all the validation steps.
|
||||
*
|
||||
* 1. Shared serialization
|
||||
* 2. API serialization
|
||||
*
|
||||
* @param {Object} response - API response
|
||||
* @param {Object} apiConfig - Docname + method of the ctrl
|
||||
* @param {Object} apiSerializers - Target API serializers
|
||||
* @param {Object} frame
|
||||
*/
|
||||
module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => {
|
||||
debug('output');
|
||||
|
||||
const tasks = [];
|
||||
|
@ -78,27 +97,27 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, options) => {
|
|||
|
||||
if (apiSerializers.all && apiSerializers.all.before) {
|
||||
tasks.push(function allSerializeBefore() {
|
||||
return apiSerializers.all.before(response, apiConfig, options);
|
||||
return apiSerializers.all.before(response, apiConfig, frame);
|
||||
});
|
||||
}
|
||||
|
||||
if (apiSerializers[apiConfig.docName]) {
|
||||
if (apiSerializers[apiConfig.docName].all) {
|
||||
tasks.push(function serializeOptionsShared() {
|
||||
return apiSerializers[apiConfig.docName].all(response, apiConfig, options);
|
||||
return apiSerializers[apiConfig.docName].all(response, apiConfig, frame);
|
||||
});
|
||||
}
|
||||
|
||||
if (apiSerializers[apiConfig.docName][apiConfig.method]) {
|
||||
tasks.push(function serializeOptionsShared() {
|
||||
return apiSerializers[apiConfig.docName][apiConfig.method](response, apiConfig, options);
|
||||
return apiSerializers[apiConfig.docName][apiConfig.method](response, apiConfig, frame);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (apiSerializers.all && apiSerializers.all.after) {
|
||||
tasks.push(function allSerializeAfter() {
|
||||
return apiSerializers.all.after(apiConfig, options);
|
||||
return apiSerializers.all.after(apiConfig, frame);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ const utils = require('../../utils');
|
|||
const INTERNAL_OPTIONS = ['transacting', 'forUpdate'];
|
||||
|
||||
/**
|
||||
* Transform into model readable language.
|
||||
* @description Shared serializer for all requests.
|
||||
*
|
||||
* Transforms certain options from API notation into model readable language/notation.
|
||||
*
|
||||
* e.g. API uses "include", but model layer uses "withRelated".
|
||||
*/
|
||||
module.exports = {
|
||||
all(apiConfig, frame) {
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @description Helper function to prepare params for internal usages.
|
||||
*
|
||||
* e.g. "a,B,c" -> ["a", "b", "c"]
|
||||
*
|
||||
* @param {String} params
|
||||
* @return {Array}
|
||||
*/
|
||||
const trimAndLowerCase = (params) => {
|
||||
params = params || '';
|
||||
|
||||
|
|
|
@ -4,10 +4,16 @@ const common = require('../../../lib/common');
|
|||
const sequence = require('../../../lib/promise/sequence');
|
||||
|
||||
/**
|
||||
* @description Shared input validation handler.
|
||||
*
|
||||
* The shared validation handler runs the request through all the validation steps.
|
||||
*
|
||||
* 1. shared validation
|
||||
* 2. api validation
|
||||
* 1. Shared validation
|
||||
* 2. API validation
|
||||
*
|
||||
* @param {Object} apiConfig - Docname + method of the ctrl
|
||||
* @param {Object} apiValidators - Target API validators
|
||||
* @param {Object} frame
|
||||
*/
|
||||
module.exports.input = (apiConfig, apiValidators, frame) => {
|
||||
debug('input');
|
||||
|
|
|
@ -11,10 +11,22 @@ module.exports = {
|
|||
return require('./validators');
|
||||
},
|
||||
|
||||
/**
|
||||
* @description Does the request access the Content API?
|
||||
*
|
||||
* Each controller is either for the Content or for the Admin API.
|
||||
* When Ghost registers each controller, it currently passes a String "content" if the controller
|
||||
* is a Content API implementation - see index.js file.
|
||||
*
|
||||
* @TODO: Move this helper function into a utils.js file.
|
||||
* @param {Object} frame
|
||||
* @return {boolean}
|
||||
*/
|
||||
isContentAPI: (frame) => {
|
||||
return frame.apiType === 'content';
|
||||
},
|
||||
|
||||
// @TODO: Remove, not used.
|
||||
isAdminAPIKey: (frame) => {
|
||||
return frame.options.context && Object.keys(frame.options.context).length !== 0 && frame.options.context.api_key &&
|
||||
frame.options.context.api_key.type === 'admin';
|
||||
|
|
|
@ -4,6 +4,13 @@ const _ = require('lodash');
|
|||
const permissions = require('../../../services/permissions');
|
||||
const common = require('../../../lib/common');
|
||||
|
||||
/**
|
||||
* @description Handle requests, which need authentication.
|
||||
*
|
||||
* @param {Object} apiConfig - Docname & method of API ctrl
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
const nonePublicAuth = (apiConfig, frame) => {
|
||||
debug('check admin permissions');
|
||||
|
||||
|
@ -11,6 +18,10 @@ const nonePublicAuth = (apiConfig, frame) => {
|
|||
|
||||
let permissionIdentifier = frame.options.id;
|
||||
|
||||
// CASE: Target ctrl can override the identifier. The identifier is the unique identifier of the target resource
|
||||
// e.g. edit a setting -> the key of the setting
|
||||
// e.g. edit a post -> post id from url param
|
||||
// e.g. change user password -> user id inside of the body structure
|
||||
if (apiConfig.identifier) {
|
||||
permissionIdentifier = apiConfig.identifier(frame);
|
||||
}
|
||||
|
@ -51,18 +62,27 @@ const nonePublicAuth = (apiConfig, frame) => {
|
|||
});
|
||||
};
|
||||
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10735
|
||||
module.exports = {
|
||||
/**
|
||||
* @description Handle permission stage for API version v2.
|
||||
*
|
||||
* @param {Object} apiConfig - Docname & method of target ctrl.
|
||||
* @param {Object} frame
|
||||
* @return {Promise}
|
||||
*/
|
||||
handle(apiConfig, frame) {
|
||||
debug('handle');
|
||||
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10099
|
||||
frame.options.context = permissions.parseContext(frame.options.context);
|
||||
|
||||
// CASE: Content API access
|
||||
if (frame.options.context.public) {
|
||||
debug('check content permissions');
|
||||
|
||||
// @TODO: The permission layer relies on the API format from v0.1. The permission layer should define
|
||||
// it's own format and should not re-use or rely on the API format. For now we have to simulate the v0.1
|
||||
// structure. We should raise an issue asap.
|
||||
// @TODO: Remove when we drop v0.1
|
||||
// @TODO: https://github.com/TryGhost/Ghost/issues/10733
|
||||
return permissions.applyPublicRules(apiConfig.docName, apiConfig.method, {
|
||||
status: frame.options.status,
|
||||
id: frame.options.id,
|
||||
|
|
Loading…
Add table
Reference in a new issue