Extracted shared API framework to separate package

refs https://github.com/TryGhost/Toolbox/issues/363

- this API framework is standalone and should be pulled out into a
  separate package so we can define its boundaries more clearly, and
  promote better testing of smaller parts
This commit is contained in:
Daniel Lockyer 2022-08-11 16:39:37 +02:00
parent 9aa5eab5ed
commit 687e68d5de
41 changed files with 156 additions and 87 deletions

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

View File

@ -0,0 +1,23 @@
# Api Framework
API framework used by Ghost
## Usage
## Develop
This is a monorepo package.
Follow the instructions for the top-level repo.
1. `git clone` this repo & `cd` into it as usual
2. Run `yarn` to install top-level dependencies.
## Test
- `yarn lint` run just eslint
- `yarn test` run lint and tests

View File

@ -0,0 +1 @@
module.exports = require('./lib/api-framework');

View File

@ -21,5 +21,9 @@ module.exports = {
get serializers() {
return require('./serializers');
},
get utils() {
return require('./utils');
}
};

View File

@ -0,0 +1,34 @@
{
"name": "@tryghost/api-framework",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/api-framework",
"author": "Ghost Foundation",
"private": true,
"main": "index.js",
"scripts": {
"dev": "echo \"Implement me!\"",
"test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura mocha './test/**/*.test.js'",
"lint:code": "eslint *.js lib/ --ext .js --cache",
"lint": "yarn lint:code && yarn lint:test",
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
},
"files": [
"index.js",
"lib"
],
"devDependencies": {
"c8": "7.12.0",
"mocha": "10.0.0",
"should": "13.2.3",
"sinon": "14.0.0"
},
"dependencies": {
"@tryghost/debug": "0.1.18",
"@tryghost/errors": "1.2.15",
"@tryghost/promise": "0.1.21",
"@tryghost/tpl": "0.1.18",
"@tryghost/validator": "0.1.27",
"jsonpath": "1.1.1",
"lodash": "4.17.21"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View File

@ -1,5 +1,5 @@
const should = require('should');
const shared = require('../../../../core/server/api/shared');
const shared = require('../');
describe('Unit: api/shared/frame', function () {
it('constructor', function () {
@ -37,7 +37,7 @@ describe('Unit: api/shared/frame', function () {
should.exist(frame.data.posts);
});
it('transform', function () {
it('transform with query', function () {
const original = {
context: {user: 'id'},
body: {posts: []},
@ -78,7 +78,7 @@ describe('Unit: api/shared/frame', function () {
should.exist(frame.options.slug);
});
it('transform', function () {
it('transform with data', function () {
const original = {
context: {user: 'id'},
options: {

View File

@ -1,5 +1,4 @@
const should = require('should');
const shared = require('../../../../core/server/api/shared');
const shared = require('../');
describe('Unit: api/shared/headers', function () {
it('empty headers config', function () {
@ -13,7 +12,7 @@ describe('Unit: api/shared/headers', function () {
return shared.headers.get({}, {disposition: {type: 'json', value: 'value'}})
.then((result) => {
result.should.eql({
'Content-Disposition': 'Attachment; filename=\"value\"',
'Content-Disposition': 'Attachment; filename="value"',
'Content-Type': 'application/json',
'Content-Length': 2
});
@ -24,7 +23,7 @@ describe('Unit: api/shared/headers', function () {
return shared.headers.get({}, {disposition: {type: 'csv', value: 'my.csv'}})
.then((result) => {
result.should.eql({
'Content-Disposition': 'Attachment; filename=\"my.csv\"',
'Content-Disposition': 'Attachment; filename="my.csv"',
'Content-Type': 'text/csv'
});
});
@ -34,7 +33,7 @@ describe('Unit: api/shared/headers', function () {
return shared.headers.get('yaml file', {disposition: {type: 'yaml', value: 'my.yaml'}})
.then((result) => {
result.should.eql({
'Content-Disposition': 'Attachment; filename=\"my.yaml\"',
'Content-Disposition': 'Attachment; filename="my.yaml"',
'Content-Type': 'application/yaml',
'Content-Length': 11
});

View File

@ -1,6 +1,6 @@
const should = require('should');
const sinon = require('sinon');
const shared = require('../../../../core/server/api/shared');
const shared = require('../');
describe('Unit: api/shared/http', function () {
let req;
@ -73,7 +73,7 @@ describe('Unit: api/shared/http', function () {
shared.http(apiImpl)(req, res, next);
});
it('api response is fn', function (done) {
it('api response is fn (data)', function (done) {
const apiImpl = sinon.stub().resolves('data');
next.callsFake(done);

View File

@ -1,8 +1,7 @@
const errors = require('@tryghost/errors');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const shared = require('../../../../core/server/api/shared');
const shared = require('../');
describe('Unit: api/shared/pipeline', function () {
afterEach(function () {

View File

@ -1,8 +1,7 @@
const errors = require('@tryghost/errors');
const should = require('should');
const Promise = require('bluebird');
const sinon = require('sinon');
const shared = require('../../../../../core/server/api/shared');
const shared = require('../../');
describe('Unit: api/shared/serializers/handle', function () {
afterEach(function () {

View File

@ -1,5 +1,5 @@
const should = require('should');
const shared = require('../../../../../../core/server/api/shared');
const shared = require('../../../');
describe('Unit: utils/serializers/input/all', function () {
describe('all', function () {

View File

@ -1,5 +1,4 @@
const should = require('should');
const optionsUtil = require('../../../../../core/server/api/shared/utils/options');
const optionsUtil = require('../../lib/utils/options');
describe('Unit: api/shared/util/options', function () {
it('returns an array with empty string when no parameters are passed', function () {

View File

@ -1,7 +1,7 @@
const errors = require('@tryghost/errors');
const Promise = require('bluebird');
const sinon = require('sinon');
const shared = require('../../../../../core/server/api/shared');
const shared = require('../../');
describe('Unit: api/shared/validators/handle', function () {
afterEach(function () {

View File

@ -2,7 +2,7 @@ const errors = require('@tryghost/errors');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const shared = require('../../../../../../core/server/api/shared');
const shared = require('../../../');
describe('Unit: api/shared/validators/input/all', function () {
afterEach(function () {
@ -287,7 +287,7 @@ describe('Unit: api/shared/validators/input/all', function () {
});
});
it('fails', function () {
it('fails with docName', function () {
const frame = {
data: {
docName: true
@ -305,7 +305,7 @@ describe('Unit: api/shared/validators/input/all', function () {
});
});
it('fails', function () {
it('fails for required field', function () {
const frame = {
data: {
docName: [{
@ -331,7 +331,7 @@ describe('Unit: api/shared/validators/input/all', function () {
});
});
it('fails', function () {
it('fails for invalid field', function () {
const frame = {
data: {
docName: [{

View File

@ -8,7 +8,7 @@
],
"statements": 57,
"branches": 85,
"functions": 52,
"functions": 51,
"lines": 57,
"include": [
"core/{*.js,frontend,server,shared}"

View File

@ -1,4 +1,4 @@
const shared = require('../shared');
const apiFramework = require('@tryghost/api-framework');
const localUtils = require('./utils');
// ESLint Override Notice
@ -9,19 +9,19 @@ const localUtils = require('./utils');
module.exports = {
get authentication() {
return shared.pipeline(require('./authentication'), localUtils);
return apiFramework.pipeline(require('./authentication'), localUtils);
},
get db() {
return shared.pipeline(require('./db'), localUtils);
return apiFramework.pipeline(require('./db'), localUtils);
},
get identities() {
return shared.pipeline(require('./identities'), localUtils);
return apiFramework.pipeline(require('./identities'), localUtils);
},
get integrations() {
return shared.pipeline(require('./integrations'), localUtils);
return apiFramework.pipeline(require('./integrations'), localUtils);
},
// @TODO: transform
@ -30,147 +30,147 @@ module.exports = {
},
get schedules() {
return shared.pipeline(require('./schedules'), localUtils);
return apiFramework.pipeline(require('./schedules'), localUtils);
},
get pages() {
return shared.pipeline(require('./pages'), localUtils);
return apiFramework.pipeline(require('./pages'), localUtils);
},
get redirects() {
return shared.pipeline(require('./redirects'), localUtils);
return apiFramework.pipeline(require('./redirects'), localUtils);
},
get roles() {
return shared.pipeline(require('./roles'), localUtils);
return apiFramework.pipeline(require('./roles'), localUtils);
},
get slugs() {
return shared.pipeline(require('./slugs'), localUtils);
return apiFramework.pipeline(require('./slugs'), localUtils);
},
get webhooks() {
return shared.pipeline(require('./webhooks'), localUtils);
return apiFramework.pipeline(require('./webhooks'), localUtils);
},
get posts() {
return shared.pipeline(require('./posts'), localUtils);
return apiFramework.pipeline(require('./posts'), localUtils);
},
get invites() {
return shared.pipeline(require('./invites'), localUtils);
return apiFramework.pipeline(require('./invites'), localUtils);
},
get mail() {
return shared.pipeline(require('./mail'), localUtils);
return apiFramework.pipeline(require('./mail'), localUtils);
},
get notifications() {
return shared.pipeline(require('./notifications'), localUtils);
return apiFramework.pipeline(require('./notifications'), localUtils);
},
get settings() {
return shared.pipeline(require('./settings'), localUtils);
return apiFramework.pipeline(require('./settings'), localUtils);
},
get membersStripeConnect() {
return shared.pipeline(require('./members-stripe-connect'), localUtils);
return apiFramework.pipeline(require('./members-stripe-connect'), localUtils);
},
get members() {
return shared.pipeline(require('./members'), localUtils);
return apiFramework.pipeline(require('./members'), localUtils);
},
get offers() {
return shared.pipeline(require('./offers'), localUtils);
return apiFramework.pipeline(require('./offers'), localUtils);
},
get tiers() {
return shared.pipeline(require('./tiers'), localUtils);
return apiFramework.pipeline(require('./tiers'), localUtils);
},
get memberSigninUrls() {
return shared.pipeline(require('./member-signin-urls.js'), localUtils);
return apiFramework.pipeline(require('./member-signin-urls.js'), localUtils);
},
get labels() {
return shared.pipeline(require('./labels'), localUtils);
return apiFramework.pipeline(require('./labels'), localUtils);
},
get images() {
return shared.pipeline(require('./images'), localUtils);
return apiFramework.pipeline(require('./images'), localUtils);
},
get media() {
return shared.pipeline(require('./media'), localUtils);
return apiFramework.pipeline(require('./media'), localUtils);
},
get files() {
return shared.pipeline(require('./files'), localUtils);
return apiFramework.pipeline(require('./files'), localUtils);
},
get tags() {
return shared.pipeline(require('./tags'), localUtils);
return apiFramework.pipeline(require('./tags'), localUtils);
},
get users() {
return shared.pipeline(require('./users'), localUtils);
return apiFramework.pipeline(require('./users'), localUtils);
},
get previews() {
return shared.pipeline(require('./previews'), localUtils);
return apiFramework.pipeline(require('./previews'), localUtils);
},
get emailPost() {
return shared.pipeline(require('./email-post'), localUtils);
return apiFramework.pipeline(require('./email-post'), localUtils);
},
get oembed() {
return shared.pipeline(require('./oembed'), localUtils);
return apiFramework.pipeline(require('./oembed'), localUtils);
},
get slack() {
return shared.pipeline(require('./slack'), localUtils);
return apiFramework.pipeline(require('./slack'), localUtils);
},
get config() {
return shared.pipeline(require('./config'), localUtils);
return apiFramework.pipeline(require('./config'), localUtils);
},
get explore() {
return shared.pipeline(require('./explore'), localUtils);
return apiFramework.pipeline(require('./explore'), localUtils);
},
get themes() {
return shared.pipeline(require('./themes'), localUtils);
return apiFramework.pipeline(require('./themes'), localUtils);
},
get actions() {
return shared.pipeline(require('./actions'), localUtils);
return apiFramework.pipeline(require('./actions'), localUtils);
},
get email_previews() {
return shared.pipeline(require('./email-previews'), localUtils);
return apiFramework.pipeline(require('./email-previews'), localUtils);
},
get emails() {
return shared.pipeline(require('./emails'), localUtils);
return apiFramework.pipeline(require('./emails'), localUtils);
},
get site() {
return shared.pipeline(require('./site'), localUtils);
return apiFramework.pipeline(require('./site'), localUtils);
},
get snippets() {
return shared.pipeline(require('./snippets'), localUtils);
return apiFramework.pipeline(require('./snippets'), localUtils);
},
get stats() {
return shared.pipeline(require('./stats'), localUtils);
return apiFramework.pipeline(require('./stats'), localUtils);
},
get customThemeSettings() {
return shared.pipeline(require('./custom-theme-settings'), localUtils);
return apiFramework.pipeline(require('./custom-theme-settings'), localUtils);
},
get serializers() {
@ -178,11 +178,11 @@ module.exports = {
},
get newsletters() {
return shared.pipeline(require('./newsletters'), localUtils);
return apiFramework.pipeline(require('./newsletters'), localUtils);
},
get comments() {
return shared.pipeline(require('./comments'), localUtils);
return apiFramework.pipeline(require('./comments'), localUtils);
},
/**
@ -194,38 +194,38 @@ module.exports = {
* `api.admin` soon. Need to figure out how serializers & validation works then.
*/
get pagesPublic() {
return shared.pipeline(require('./pages-public'), localUtils, 'content');
return apiFramework.pipeline(require('./pages-public'), localUtils, 'content');
},
get tagsPublic() {
return shared.pipeline(require('./tags-public'), localUtils, 'content');
return apiFramework.pipeline(require('./tags-public'), localUtils, 'content');
},
get publicSettings() {
return shared.pipeline(require('./settings-public'), localUtils, 'content');
return apiFramework.pipeline(require('./settings-public'), localUtils, 'content');
},
get postsPublic() {
return shared.pipeline(require('./posts-public'), localUtils, 'content');
return apiFramework.pipeline(require('./posts-public'), localUtils, 'content');
},
get authorsPublic() {
return shared.pipeline(require('./authors-public'), localUtils, 'content');
return apiFramework.pipeline(require('./authors-public'), localUtils, 'content');
},
get tiersPublic() {
return shared.pipeline(require('./tiers-public'), localUtils, 'content');
return apiFramework.pipeline(require('./tiers-public'), localUtils, 'content');
},
get newslettersPublic() {
return shared.pipeline(require('./newsletters-public'), localUtils, 'content');
return apiFramework.pipeline(require('./newsletters-public'), localUtils, 'content');
},
get offersPublic() {
return shared.pipeline(require('./offers-public'), localUtils, 'content');
return apiFramework.pipeline(require('./offers-public'), localUtils, 'content');
},
get commentsMembers() {
return shared.pipeline(require('./comments-members'), localUtils, 'members');
return apiFramework.pipeline(require('./comments-members'), localUtils, 'members');
}
};

View File

@ -1,6 +1,6 @@
const _ = require('lodash');
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:input:db');
const optionsUtil = require('../../../../shared/utils/options');
const optionsUtil = require('@tryghost/api-framework').utils.options;
const INTERNAL_OPTIONS = ['transacting', 'forUpdate'];

View File

@ -3,7 +3,7 @@ const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output
const allowedIncludes = ['monthly_price', 'yearly_price'];
const localUtils = require('../../index');
const utils = require('../../../../shared/utils');
const {utils} = require('@tryghost/api-framework');
const labs = require('../../../../../../shared/labs');
module.exports = {

View File

@ -1,3 +1 @@
module.exports.endpoints = require('./endpoints');
module.exports.shared = require('./shared');

View File

@ -5,7 +5,7 @@ const urlUtils = require('../../../shared/url-utils');
const labs = require('../../../shared/labs');
const moment = require('moment-timezone');
const api = require('../../api').endpoints;
const apiShared = require('../../api').shared;
const apiFramework = require('@tryghost/api-framework');
const {URL} = require('url');
const mobiledocLib = require('../../lib/mobiledoc');
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
@ -104,7 +104,7 @@ const serializePostModel = async (model) => {
const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
const docName = 'posts';
await apiShared
await apiFramework
.serializers
.handle
.output(model, {docName: docName, method: 'read'}, api.serializers.output, frame);

View File

@ -2,7 +2,7 @@ module.exports = (event, model) => {
const _ = require('lodash');
const {sequence} = require('@tryghost/promise');
const api = require('../../api').endpoints;
const apiShared = require('../../api').shared;
const apiFramework = require('@tryghost/api-framework');
const resourceName = event.match(/(\w+)\./)[1];
const docName = `${resourceName}s`;
@ -23,7 +23,7 @@ module.exports = (event, model) => {
};
}
return apiShared
return apiFramework
.serializers
.handle
.output(model, {docName: docName, method: 'read'}, api.serializers.output, frame)
@ -46,7 +46,7 @@ module.exports = (event, model) => {
frame.options.withRelated = ['tags', 'authors'];
}
return apiShared
return apiFramework
.serializers
.handle
.output(model, {docName: docName, method: 'read'}, api.serializers.output, frame)

View File

@ -1,6 +1,6 @@
const express = require('../../../../../shared/express');
const api = require('../../../../api').endpoints;
const http = require('../../../../api').shared.http;
const {http} = require('@tryghost/api-framework');
const apiMw = require('../../middleware');
const mw = require('./middleware');

View File

@ -1,7 +1,7 @@
const express = require('../../../../../shared/express');
const cors = require('cors');
const api = require('../../../../api').endpoints;
const http = require('../../../../api').shared.http;
const {http} = require('@tryghost/api-framework');
const mw = require('./middleware');
const config = require('../../../../../shared/config');

View File

@ -1,6 +1,6 @@
const express = require('../../../shared/express');
const api = require('../../api').endpoints;
const http = require('../../api').shared.http;
const {http} = require('@tryghost/api-framework');
const bodyParser = require('body-parser');
const membersService = require('../../../server/services/members');

View File

@ -55,6 +55,7 @@
"@sentry/node": "7.9.0",
"@tryghost/adapter-manager": "0.0.0",
"@tryghost/admin-api-schema": "4.1.1",
"@tryghost/api-framework": "0.0.0",
"@tryghost/api-version-compatibility-service": "0.0.0",
"@tryghost/bookshelf-plugins": "0.5.0",
"@tryghost/bootstrap-socket": "0.0.0",