Renamed /users to /authors for Content API V2 (#10096)

refs #10061

- Made /authors endpoint available in Content API V2
This commit is contained in:
Katharina Irrgang 2018-11-07 15:29:37 +01:00 committed by Naz Gargol
parent 3b8621e19c
commit ff6bf5f318
34 changed files with 494 additions and 339 deletions

View File

@ -0,0 +1,66 @@
const Promise = require('bluebird');
const common = require('../../lib/common');
const models = require('../../models');
const ALLOWED_INCLUDES = ['count.posts'];
module.exports = {
docName: 'authors',
browse: {
options: [
'include',
'filter',
'fields',
'limit',
'status',
'order',
'page'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
}
}
},
permissions: true,
query(frame) {
return models.User.findPage(frame.options);
}
},
read: {
options: [
'include',
'filter',
'fields'
],
data: [
'id',
'slug',
'status',
'email',
'role'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
}
}
},
permissions: true,
query(frame) {
return models.User.findOne(frame.data, frame.options)
.then((model) => {
if (!model) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.authors.notFound')
}));
}
return model;
});
}
}
};

View File

@ -77,5 +77,9 @@ module.exports = {
get slack() {
return shared.pipeline(require('./slack'), localUtils);
},
get authors() {
return shared.pipeline(require('./authors'), localUtils);
}
};

View File

@ -0,0 +1,25 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:authors');
const mapper = require('./utils/mapper');
module.exports = {
browse(models, apiConfig, frame) {
debug('browse');
frame.response = {
authors: models.data.map(model => mapper.mapUser(model, frame)),
meta: models.meta
};
debug(frame.response);
},
read(model, apiConfig, frame) {
debug('read');
frame.response = {
authors: [mapper.mapUser(model, frame)]
};
debug(frame.response);
}
};

View File

@ -61,5 +61,9 @@ module.exports = {
get oembed() {
return require('./oembed');
},
get authors() {
return require('./authors');
}
};

View File

@ -64,7 +64,7 @@ function getPostData(req, res, next) {
}
// @NOTE: amp is not supported for static pages
helpers.entryLookup(urlWithoutSubdirectoryWithoutAmp, {permalinks, resourceType: 'posts'}, res.locals)
helpers.entryLookup(urlWithoutSubdirectoryWithoutAmp, {permalinks, query: {resource: 'posts'}}, res.locals)
.then((result) => {
if (result && result.entry) {
req.body.post = result.entry;

View File

@ -37,6 +37,9 @@ const RESOURCES = {
pages: {
alias: 'pages',
resource: 'posts'
},
authors: {
alias: 'authors'
}
};
@ -145,6 +148,13 @@ get = function get(resource, options) {
const controller = api[apiVersion][RESOURCES[resource].alias] ? RESOURCES[resource].alias : RESOURCES[resource].resource;
const action = isBrowse(apiOptions) ? 'browse' : 'read';
// CASE: no fallback defined e.g. v0.1 tries to fetch "authors"
if (!controller) {
data.error = i18n.t('warnings.helpers.get.invalidResource');
logging.warn(data.error);
return Promise.resolve(options.inverse(self, {data: data}));
}
// Parse the options we're going to pass to the API
apiOptions = parseOptions(this, apiOptions);

View File

@ -341,6 +341,7 @@ User = ghostBookshelf.Model.extend({
// CASE: The `withRelated` parameter is allowed when using the public API, but not the `roles` value.
// Otherwise we expose too much information.
// @TODO: the target controller should define the allowed includes, but not the model layer O_O (https://github.com/TryGhost/Ghost/issues/10106)
if (options && options.context && options.context.public) {
if (options.withRelated && options.withRelated.indexOf('roles') !== -1) {
options.withRelated.splice(options.withRelated.indexOf('roles'), 1);

View File

@ -91,6 +91,10 @@ class CollectionRouter extends ParentRouter {
order: this.order,
permalinks: this.permalinks.getValue({withUrlOptions: true}),
resourceType: this.getResourceType(),
query: {
alias: 'posts',
resource: 'posts'
},
context: this.context,
frontPageTemplate: 'home',
templates: this.templates,

View File

@ -20,7 +20,10 @@ class PreviewRouter extends ParentRouter {
_prepareContext(req, res, next) {
res.routerOptions = {
type: 'entry',
resourceType: 'preview'
query: {
alias: 'preview',
resource: 'posts'
}
};
next();

View File

@ -39,6 +39,10 @@ class StaticPagesRouter extends ParentRouter {
filter: this.filter,
permalinks: this.permalinks.getValue(),
resourceType: this.getResourceType(),
query: {
alias: 'pages',
resource: 'posts'
},
context: ['page']
};

View File

@ -54,7 +54,7 @@ class TaxonomyRouter extends ParentRouter {
type: 'channel',
name: this.taxonomyKey,
permalinks: this.permalinks.getValue(),
data: {[this.taxonomyKey]: _.omit(RESOURCE_CONFIG.QUERY[this.taxonomyKey], ['alias', 'internal'])},
data: {[this.taxonomyKey]: _.omit(RESOURCE_CONFIG.QUERY[this.taxonomyKey], ['internal'])},
filter: RESOURCE_CONFIG.TAXONOMIES[this.taxonomyKey].filter,
resourceType: this.getResourceType(),
context: [this.taxonomyKey],
@ -70,7 +70,7 @@ class TaxonomyRouter extends ParentRouter {
}
getResourceType() {
return RESOURCE_CONFIG.QUERY[this.taxonomyKey].resource;
return RESOURCE_CONFIG.QUERY[this.taxonomyKey].alias;
}
getRoute() {

View File

@ -10,8 +10,7 @@ module.exports.QUERY = {
}
},
author: {
internal: true,
alias: 'users',
alias: 'authors',
type: 'read',
resource: 'users',
options: {
@ -20,7 +19,7 @@ module.exports.QUERY = {
}
},
user: {
alias: 'users',
alias: 'authors',
type: 'read',
resource: 'users',
options: {

View File

@ -14,20 +14,10 @@ module.exports = function previewController(req, res, next) {
include: 'author,authors,tags'
};
let resourceType = res.routerOptions.resourceType;
/**
* @TODO:
* Remove fallback to posts if we drop v0.1.
*/
if (!api[resourceType]) {
resourceType = 'posts';
}
api[resourceType]
(api[res.routerOptions.query.alias] || api[res.routerOptions.query.resource])
.read(params)
.then(function then(result) {
const post = result[resourceType][0];
const post = (result[res.routerOptions.query.alias] || result[res.routerOptions.query.resource])[0];
if (!post) {
return next();

View File

@ -8,7 +8,7 @@ function processQuery(query, locals) {
query = _.cloneDeep(query);
// Return a promise for the api query
return api[query.resource][query.type](query.options);
return (api[query.alias] || api[query.resource])[query.type](query.options);
}
module.exports = function staticController(req, res, next) {
@ -31,7 +31,7 @@ module.exports = function staticController(req, res, next) {
if (config.type === 'browse') {
response.data[name] = result[name];
} else {
response.data[name] = result[name][config.resource];
response.data[name] = result[name][config.alias] || result[name][config.resource];
}
});
}

View File

@ -30,22 +30,14 @@ function entryLookup(postUrl, routerOptions, locals) {
isEditURL = true;
}
let resourceType = routerOptions.resourceType;
// @NOTE: v0.1 does not have a pages controller.
// @TODO: remove me when we drop v0.1
if (!api[resourceType]) {
resourceType = 'posts';
}
/**
* Query database to find entry.
* @deprecated: `author`, will be removed in Ghost 3.0
*/
return api[resourceType]
return (api[routerOptions.query.alias] || api[routerOptions.query.resource])
.read(_.extend(_.pick(params, 'slug', 'id'), {include: 'author,authors,tags'}))
.then(function then(result) {
const entry = result[resourceType][0];
const entry = (result[routerOptions.query.alias] || result[routerOptions.query.resource])[0];
if (!entry) {
return Promise.resolve();

View File

@ -2,27 +2,31 @@
* # Fetch Data
* Dynamically build and execute queries on the API
*/
const _ = require('lodash'),
Promise = require('bluebird'),
defaultPostQuery = {};
const _ = require('lodash');
const Promise = require('bluebird');
// The default settings for a default post query
const queryDefaults = {
type: 'browse',
resource: 'posts',
alias: 'posts',
options: {}
};
/**
* Default post query needs to always include author, authors & tags
*
* @deprecated: `author`, will be removed in Ghost 3.0
*/
_.extend(defaultPostQuery, queryDefaults, {
const defaultQueryOptions = {
options: {
include: 'author,authors,tags'
}
});
};
/**
* Default post query needs to always include author, authors & tags
*/
const defaultPostQuery = _.cloneDeep(queryDefaults);
defaultPostQuery.options = defaultQueryOptions.options;
/**
* ## Process Query
@ -39,7 +43,6 @@ function processQuery(query, slugParam, locals) {
query = _.cloneDeep(query);
// Ensure that all the properties are filled out
_.defaultsDeep(query, queryDefaults);
// Replace any slugs, see TaxonomyRouter. We replace any '%s' by the slug
@ -48,7 +51,7 @@ function processQuery(query, slugParam, locals) {
});
// Return a promise for the api query
return api[query.resource][query.type](query.options);
return (api[query.alias] || api[query.resource])[query.type](query.options);
}
/**
@ -100,7 +103,7 @@ function fetchData(pathOptions, routerOptions, locals) {
if (config.type === 'browse') {
response.data[name] = results[name];
} else {
response.data[name] = results[name][config.resource];
response.data[name] = results[name][config.alias] || results[name][config.resource];
}
});
}

View File

@ -48,7 +48,6 @@ _private.validateData = function validateData(object) {
let [resourceKey, slug] = shortForm.split('.');
// @NOTE: `data: author.foo` is not allowed currently, because this will make {{author}} available in the theme, which is deprecated (single author usage)
if (!RESOURCE_CONFIG.QUERY[resourceKey] ||
(RESOURCE_CONFIG.QUERY[resourceKey].hasOwnProperty('internal') && RESOURCE_CONFIG.QUERY[resourceKey].internal === true)) {
throw new common.errors.ValidationError({
@ -58,7 +57,7 @@ _private.validateData = function validateData(object) {
}
longForm.query[options.resourceKey || resourceKey] = {};
longForm.query[options.resourceKey || resourceKey] = _.omit(_.cloneDeep(RESOURCE_CONFIG.QUERY[resourceKey]), 'alias');
longForm.query[options.resourceKey || resourceKey] = _.cloneDeep(RESOURCE_CONFIG.QUERY[resourceKey]);
// redirect is enabled by default when using the short form
longForm.router = {
@ -148,6 +147,7 @@ _private.validateData = function validateData(object) {
});
const DEFAULT_RESOURCE = _.find(RESOURCE_CONFIG.QUERY, {resource: data.query[key].resource});
data.query[key] = _.defaults(data.query[key], _.omit(DEFAULT_RESOURCE, 'options'));
data.query[key].options = _.pick(object.data[key], allowedQueryOptions);
if (data.query[key].type === 'read') {

View File

@ -109,7 +109,7 @@ const resourcesConfig = [
}
},
{
type: 'users',
type: 'authors',
modelOptions: {
modelName: 'User',
exclude: [

View File

@ -349,6 +349,9 @@
"posts": {
"postNotFound": "Post not found."
},
"authors": {
"notFound": "Author not found."
},
"pages": {
"pageNotFound": "Page not found."
},

View File

@ -16,9 +16,9 @@ module.exports = function apiRoutes() {
router.get('/pages/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.pages.read));
// ## Users
router.get('/users', mw.authenticatePublic, apiv2.http(apiv2.users.browse));
router.get('/users/:id', mw.authenticatePublic, apiv2.http(apiv2.users.read));
router.get('/users/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.users.read));
router.get('/authors', mw.authenticatePublic, apiv2.http(apiv2.authors.browse));
router.get('/authors/:id', mw.authenticatePublic, apiv2.http(apiv2.authors.read));
router.get('/authors/slug/:slug', mw.authenticatePublic, apiv2.http(apiv2.authors.read));
// ## Tags
router.get('/tags', mw.authenticatePublic, apiv2.http(apiv2.tags.browse));

View File

@ -0,0 +1,195 @@
const should = require('should');
const supertest = require('supertest');
const _ = require('lodash');
const url = require('url');
const configUtils = require('../../../../utils/configUtils');
const config = require('../../../../../../core/server/config');
const models = require('../../../../../../core/server/models');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Authors Content API V2', function () {
let ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return testUtils.initFixtures('users:no-owner', 'user:inactive', 'posts', 'api_keys');
});
});
afterEach(function () {
configUtils.restore();
});
const validKey = localUtils.getValidKey();
it('browse authors', function (done) {
request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.authors);
testUtils.API.checkResponse(jsonResponse, 'authors');
jsonResponse.authors.should.have.length(7);
// We don't expose the email address, status and other attrs.
testUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['url'], null, null, {public: true});
should.exist(res.body.authors[0].url);
should.exist(url.parse(res.body.authors[0].url).protocol);
should.exist(url.parse(res.body.authors[0].url).host);
// Public api returns all authors, but no status! Locked/Inactive authors can still have written articles.
models.User.findPage(Object.assign({status: 'all'}, testUtils.context.internal))
.then((response) => {
_.map(response.data, (model) => model.toJSON()).length.should.eql(7);
done();
});
});
});
it('browse authors: throws error if trying to fetch roles', function (done) {
request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&include=roles`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(422)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
done();
});
});
it('browse user by slug: count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`authors/slug/ghost/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.authors);
jsonResponse.authors.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null, {public: true});
done();
});
});
it('browse user by id: count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`authors/1/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.authors);
jsonResponse.authors.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null, {public: true});
done();
});
});
it('browse user with count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&include=count.posts&order=count.posts ASC`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.authors);
jsonResponse.authors.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null, {public: true});
// Each user should have the correct count
_.find(jsonResponse.authors, {slug:'joe-bloggs'}).count.posts.should.eql(4);
_.find(jsonResponse.authors, {slug:'contributor'}).count.posts.should.eql(0);
_.find(jsonResponse.authors, {slug:'slimer-mcectoplasm'}).count.posts.should.eql(0);
_.find(jsonResponse.authors, {slug:'jimothy-bogendath'}).count.posts.should.eql(0);
_.find(jsonResponse.authors, {slug: 'smith-wellingsworth'}).count.posts.should.eql(0);
_.find(jsonResponse.authors, {slug:'ghost'}).count.posts.should.eql(7);
_.find(jsonResponse.authors, {slug:'inactive'}).count.posts.should.eql(0);
const ids = jsonResponse.authors
.filter(author => (author.slug !== 'ghost'))
.filter(author => (author.slug !== 'inactive'))
.map(user=> user.id);
ids.should.eql([
testUtils.DataGenerator.Content.users[1].id,
testUtils.DataGenerator.Content.users[2].id,
testUtils.DataGenerator.Content.users[3].id,
testUtils.DataGenerator.Content.users[7].id,
testUtils.DataGenerator.Content.users[0].id
]);
done();
});
});
it('browse authors: post count', function (done) {
request.get(localUtils.API.getApiQuery(`authors/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.authors);
testUtils.API.checkResponse(jsonResponse, 'authors');
jsonResponse.authors.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.authors[0], 'author', ['count', 'url'], null, null, {public: true});
done();
});
});
});

View File

@ -1,247 +0,0 @@
const should = require('should');
const supertest = require('supertest');
const _ = require('lodash');
const url = require('url');
const configUtils = require('../../../../utils/configUtils');
const config = require('../../../../../../core/server/config');
const models = require('../../../../../../core/server/models');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Users Content API V2', function () {
let ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return testUtils.initFixtures('users:no-owner', 'user:inactive', 'posts', 'api_keys');
});
});
afterEach(function () {
configUtils.restore();
});
const validKey = localUtils.getValidKey();
it('browse users', function (done) {
request.get(localUtils.API.getApiQuery(`users/?key=${validKey}`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(7);
// We don't expose the email address, status and other attrs.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['url'], null, null, {public: true});
should.exist(res.body.users[0].url);
should.exist(url.parse(res.body.users[0].url).protocol);
should.exist(url.parse(res.body.users[0].url).host);
// Public api returns all users, but no status! Locked/Inactive users can still have written articles.
models.User.findPage(Object.assign({status: 'all'}, testUtils.context.internal))
.then((response) => {
_.map(response.data, (model) => model.toJSON()).length.should.eql(7);
done();
});
});
});
it('browse users: ignores fetching roles', function (done) {
request.get(localUtils.API.getApiQuery(`users/?key=${validKey}&include=roles`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['url'], null, null, {public: true});
done();
});
});
it('browse user by slug: ignores fetching roles', function (done) {
request.get(localUtils.API.getApiQuery(`users/slug/ghost/?key=${validKey}&include=roles`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['url'], null, null, {public: true});
done();
});
});
it('browse user by slug: count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`users/slug/ghost/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count', 'url'], null, null, {public: true});
done();
});
});
it('browse user by id: count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`users/1/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(1);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count', 'url'], null, null, {public: true});
done();
});
});
it('browse user with count.posts', function (done) {
request.get(localUtils.API.getApiQuery(`users/?key=${validKey}&include=count.posts&order=count.posts ASC`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count', 'url'], null, null, {public: true});
// Each user should have the correct count
_.find(jsonResponse.users, {slug:'joe-bloggs'}).count.posts.should.eql(4);
_.find(jsonResponse.users, {slug:'contributor'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'slimer-mcectoplasm'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'jimothy-bogendath'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug: 'smith-wellingsworth'}).count.posts.should.eql(0);
_.find(jsonResponse.users, {slug:'ghost'}).count.posts.should.eql(7);
_.find(jsonResponse.users, {slug:'inactive'}).count.posts.should.eql(0);
const ids = jsonResponse.users
.filter(user => (user.slug !== 'ghost'))
.filter(user => (user.slug !== 'inactive'))
.map(user=> user.id);
ids.should.eql([
testUtils.DataGenerator.Content.users[1].id,
testUtils.DataGenerator.Content.users[2].id,
testUtils.DataGenerator.Content.users[3].id,
testUtils.DataGenerator.Content.users[7].id,
testUtils.DataGenerator.Content.users[0].id
]);
done();
});
});
it('browse user by id: ignores fetching roles', function (done) {
request.get(localUtils.API.getApiQuery(`users/1/?key=${validKey}&include=roles`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
jsonResponse.users.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['url'], null, null, {public: true});
done();
});
});
it('browse users: post count', function (done) {
request.get(localUtils.API.getApiQuery(`users/?key=${validKey}&include=count.posts`))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse.users);
testUtils.API.checkResponse(jsonResponse, 'users');
jsonResponse.users.should.have.length(7);
// We don't expose the email address.
testUtils.API.checkResponse(jsonResponse.users[0], 'user', ['count', 'url'], null, null, {public: true});
done();
});
});
});

View File

@ -79,7 +79,7 @@ describe('Integration: services/url/UrlService', function () {
});
router2.getFilter.returns(false);
router2.getResourceType.returns('users');
router2.getResourceType.returns('authors');
router2.getPermalinks.returns({
getValue: function () {
return '/author/:slug/';
@ -147,7 +147,7 @@ describe('Integration: services/url/UrlService', function () {
generator.getUrls().length.should.eql(5);
}
if (generator.router.getResourceType() === 'users') {
if (generator.router.getResourceType() === 'authors') {
generator.getUrls().length.should.eql(5);
}
});
@ -377,7 +377,7 @@ describe('Integration: services/url/UrlService', function () {
});
router3.getFilter.returns(false);
router3.getResourceType.returns('users');
router3.getResourceType.returns('authors');
router3.getPermalinks.returns({
getValue: function () {
return '/persons/:slug/';
@ -451,7 +451,7 @@ describe('Integration: services/url/UrlService', function () {
generator.getUrls().length.should.eql(5);
}
if (generator.router.getResourceType() === 'users') {
if (generator.router.getResourceType() === 'authors') {
generator.getUrls().length.should.eql(5);
}
});
@ -616,7 +616,7 @@ describe('Integration: services/url/UrlService', function () {
});
router3.getFilter.returns(false);
router3.getResourceType.returns('users');
router3.getResourceType.returns('authors');
router3.getPermalinks.returns({
getValue: function () {
return '/persons/:slug/';

View File

@ -897,6 +897,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -915,6 +916,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
apollo: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -1225,6 +1227,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -1244,6 +1247,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -1264,6 +1268,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
tag: {
alias: 'authors',
resource: 'users',
type: 'read',
options: {
@ -1288,6 +1293,7 @@ describe('Integration - Web - Site', function () {
data: {
query: {
tag: {
alias: 'authors',
resource: 'users',
type: 'read',
options: {

View File

@ -123,7 +123,7 @@ describe('Unit - apps/amp/lib/router', function () {
urlService.getPermalinkByUrl.withArgs('/welcome/').returns('/:slug/');
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', resourceType: 'posts'}).resolves({
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', query: {resource: 'posts'}}).resolves({
entry: post
});
@ -139,7 +139,7 @@ describe('Unit - apps/amp/lib/router', function () {
urlService.getPermalinkByUrl.withArgs('/welcome/').returns('/:slug/');
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', resourceType: 'posts'}).resolves({
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', query: {resource: 'posts'}}).resolves({
entry: post
});
@ -154,7 +154,7 @@ describe('Unit - apps/amp/lib/router', function () {
urlService.getPermalinkByUrl.withArgs('/welcome/').returns('/:slug/');
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', resourceType: 'posts'})
helpers.entryLookup.withArgs('/welcome/', {permalinks: '/:slug/', query: {resource: 'posts'}})
.rejects(new common.errors.NotFoundError());
ampController.getPostData(req, res, function (err) {

View File

@ -283,6 +283,29 @@ describe('{{#get}} helper', function () {
});
});
describe('authors v0.1', function () {
let browseUsersStub;
const meta = {pagination: {}};
beforeEach(function () {
browseUsersStub = sandbox.stub(api["v0.1"].users, 'browse');
browseUsersStub.returns(new Promise.resolve({users: [], meta: meta}));
});
it('browse users v0.1', function (done) {
helpers.get.call(
{},
'authors',
{hash: {}, data: locals, fn: fn, inverse: inverse}
).then(function () {
inverse.calledOnce.should.be.true();
should.exist(inverse.args[0][1].data.error);
done();
}).catch(done);
});
});
describe('users v2', function () {
let browseUsersStub;
const meta = {pagination: {}};
@ -290,9 +313,9 @@ describe('{{#get}} helper', function () {
beforeEach(function () {
locals = {root: {_locals: {apiVersion: 'v2'}}};
browseUsersStub = sandbox.stub(api["v2"], 'users').get(() => {
browseUsersStub = sandbox.stub(api["v2"], 'authors').get(() => {
return {
browse: sandbox.stub().resolves({users: [], meta: meta})
browse: sandbox.stub().resolves({authors: [], meta: meta})
};
});
});
@ -306,8 +329,40 @@ describe('{{#get}} helper', function () {
labsStub.calledOnce.should.be.true();
fn.called.should.be.true();
fn.firstCall.args[0].should.be.an.Object().with.property('users');
fn.firstCall.args[0].users.should.eql([]);
fn.firstCall.args[0].should.be.an.Object().with.property('authors');
fn.firstCall.args[0].authors.should.eql([]);
inverse.called.should.be.false();
done();
}).catch(done);
});
});
describe('authors v2', function () {
let browseUsersStub;
const meta = {pagination: {}};
beforeEach(function () {
locals = {root: {_locals: {apiVersion: 'v2'}}};
browseUsersStub = sandbox.stub(api["v2"], 'authors').get(() => {
return {
browse: sandbox.stub().resolves({authors: [], meta: meta})
};
});
});
it('browse users', function (done) {
helpers.get.call(
{},
'authors',
{hash: {}, data: locals, fn: fn, inverse: inverse}
).then(function () {
labsStub.calledOnce.should.be.true();
fn.called.should.be.true();
fn.firstCall.args[0].should.be.an.Object().with.property('authors');
fn.firstCall.args[0].authors.should.eql([]);
inverse.called.should.be.false();
done();

View File

@ -136,6 +136,7 @@ describe('UNIT - services/routing/CollectionRouter', function () {
type: 'collection',
filter: 'page:false',
permalinks: '/:slug/:options(edit)?/',
query: {alias: 'posts', resource: 'posts'},
frontPageTemplate: 'home',
templates: [],
identifier: collectionRouter.identifier,
@ -158,6 +159,7 @@ describe('UNIT - services/routing/CollectionRouter', function () {
type: 'collection',
filter: 'page:false',
permalinks: '/:slug/:options(edit)?/',
query: {alias: 'posts', resource: 'posts'},
frontPageTemplate: 'home',
templates: ['index', 'home'],
identifier: collectionRouter.identifier,

View File

@ -72,7 +72,7 @@ describe('UNIT - services/routing/TaxonomyRouter', function () {
name: 'tag',
permalinks: '/tag/:slug/',
resourceType: RESOURCE_CONFIG.QUERY.tag.resource,
data: {tag: _.omit(RESOURCE_CONFIG.QUERY.tag, 'alias')},
data: {tag: RESOURCE_CONFIG.QUERY.tag},
filter: RESOURCE_CONFIG.TAXONOMIES.tag.filter,
context: ['tag'],
slugTemplate: true,

View File

@ -46,7 +46,7 @@ describe('Unit - services/routing/controllers/preview', function () {
res = {
routerOptions: {
resourceType: 'preview'
query: {alias: 'preview', resource: 'posts'}
},
locals: {
apiVersion: 'v0.1'
@ -165,7 +165,7 @@ describe('Unit - services/routing/controllers/preview', function () {
res = {
routerOptions: {
resourceType: 'preview'
query: {alias: 'preview', resource: 'posts'}
},
locals: {
apiVersion: 'v2'

View File

@ -23,7 +23,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('static pages', function () {
const routerOptions = {
permalinks: '/:slug/',
resourceType: 'pages'
query: {alias: 'pages', resource: 'posts'}
};
let pages;
@ -54,7 +54,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('Permalinks: /:slug/', function () {
const routerOptions = {
permalinks: '/:slug/',
resourceType: 'posts'
query: {alias: 'pages', resource: 'posts'}
};
beforeEach(function () {
@ -121,7 +121,8 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('Permalinks: /:year/:month/:day/:slug/', function () {
const routerOptions = {
permalinks: '/:year/:month/:day/:slug/'
permalinks: '/:year/:month/:day/:slug/',
query: {alias: 'pages', resource: 'posts'}
};
beforeEach(function () {
@ -192,7 +193,8 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('with url options', function () {
const routerOptions = {
permalinks: '/:slug/:options(edit)?'
permalinks: '/:slug/:options(edit)?',
query: {alias: 'pages', resource: 'posts'}
};
beforeEach(function () {
@ -272,7 +274,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('static pages', function () {
const routerOptions = {
permalinks: '/:slug/',
resourceType: 'pages'
query: {alias: 'pages', resource: 'posts'}
};
let pages;
@ -323,7 +325,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('posts', function () {
const routerOptions = {
permalinks: '/:slug/',
resourceType: 'posts'
query: {alias: 'posts', resource: 'posts'}
};
let posts;

View File

@ -1,6 +1,6 @@
const should = require('should'),
sinon = require('sinon'),
api = require('../../../../../server/api'),
api = require('../../../../../server/api')['v0.1'],
helpers = require('../../../../../server/services/routing/helpers'),
testUtils = require('../../../../utils'),
sandbox = sinon.sandbox.create();
@ -154,6 +154,7 @@ describe('Unit - services/routing/helpers/fetch-data', function () {
filter: 'tags:%s',
data: {
tag: {
alias: 'tags',
type: 'read',
resource: 'tags',
options: {slug: '%s'}

View File

@ -349,6 +349,9 @@ describe('UNIT: services/settings/validate', function () {
bed: 'tag.bed',
dream: 'tag.dream'
}
},
'/lala/': {
data: 'author.carsten'
}
},
collections: {
@ -378,6 +381,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -396,6 +400,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
user: {
alias: 'authors',
resource: 'users',
type: 'read',
options: {
@ -405,7 +410,7 @@ describe('UNIT: services/settings/validate', function () {
}
},
router: {
users: [{redirect: true, slug: 'ghost'}]
authors: [{redirect: true, slug: 'ghost'}]
}
},
templates: []
@ -414,6 +419,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -432,6 +438,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
bed: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -440,6 +447,7 @@ describe('UNIT: services/settings/validate', function () {
}
},
dream: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -453,6 +461,25 @@ describe('UNIT: services/settings/validate', function () {
}
},
templates: []
},
'/lala/': {
data: {
query: {
author: {
alias: 'authors',
resource: 'users',
type: 'read',
options: {
slug: 'carsten',
visibility: 'public'
}
}
},
router: {
authors: [{redirect: true, slug: 'carsten'}]
}
},
templates: []
}
},
collections: {
@ -461,6 +488,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
home: {
alias: 'pages',
resource: 'posts',
type: 'read',
options: {
@ -481,6 +509,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
something: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -500,6 +529,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
tag: {
alias: 'tags',
resource: 'tags',
type: 'read',
options: {
@ -540,7 +570,7 @@ describe('UNIT: services/settings/validate', function () {
},
'/partyparty/': {
data: {
posts: {
people: {
resource: 'users',
type: 'read',
slug: 'djgutelaune',
@ -571,6 +601,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
food: {
alias: 'posts',
resource: 'posts',
type: 'browse',
options: {}
@ -586,6 +617,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
posts: {
alias: 'posts',
resource: 'posts',
type: 'read',
options: {
@ -604,7 +636,8 @@ describe('UNIT: services/settings/validate', function () {
'/partyparty/': {
data: {
query: {
posts: {
people: {
alias: 'authors',
resource: 'users',
type: 'read',
options: {
@ -614,7 +647,7 @@ describe('UNIT: services/settings/validate', function () {
}
},
router: {
users: [{redirect: true, slug: 'djgutelaune'}]
authors: [{redirect: true, slug: 'djgutelaune'}]
}
},
templates: []
@ -626,6 +659,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
gym: {
alias: 'posts',
resource: 'posts',
type: 'read',
options: {
@ -703,24 +737,6 @@ describe('UNIT: services/settings/validate', function () {
throw new Error('should fail');
});
it('errors: data shortform author is not allowed', function () {
try {
validate({
collections: {
'/magic/': {
permalink: '/{slug}/',
data: 'author.food'
}
}
});
} catch (err) {
(err instanceof common.errors.ValidationError).should.be.true();
return;
}
throw new Error('should fail');
});
it('errors: data longform name is author', function () {
try {
validate({

View File

@ -68,7 +68,7 @@ describe('Unit: services/url/Resources', function () {
created.tags.length.should.eql(testUtils.DataGenerator.forKnex.tags.length);
// all mocked users are active
created.users.length.should.eql(testUtils.DataGenerator.forKnex.users.length);
created.authors.length.should.eql(testUtils.DataGenerator.forKnex.users.length);
done();
});

View File

@ -14,6 +14,7 @@ var _ = require('lodash'),
posts: ['posts', 'meta'],
tags: ['tags', 'meta'],
users: ['users', 'meta'],
authors: ['authors', 'meta'],
settings: ['settings', 'meta'],
subscribers: ['subscribers', 'meta'],
roles: ['roles'],
@ -45,6 +46,22 @@ var _ = require('lodash'),
)
.value()
},
author: _(schema.users)
.keys()
.without(
'password',
'email',
'ghost_auth_access_token',
'ghost_auth_id',
'created_at',
'created_by',
'updated_at',
'updated_by',
'last_seen',
'status'
)
.value()
,
// Tag API swaps parent_id to parent
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
setting: _.keys(schema.settings),