0
Fork 0
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:
kirrg001 2019-05-06 14:24:12 +02:00
parent 79345f9030
commit a31ed7c71d
11 changed files with 241 additions and 23 deletions

View file

@ -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: {

View file

@ -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) {

View file

@ -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 = {};

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
});
}

View file

@ -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) {

View file

@ -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 || '';

View file

@ -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');

View file

@ -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';

View file

@ -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,