0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Separated pages & posts in Admin API v2 (#10494)

refs #10438, refs #10106

* Renamed existing pages ctrl
* Splitted posts & pages for Admin API v2
* Added pages JSON input schema for Admin API v2
* Removed single author for Content & Admin API v2
  - single author is not documented
  - single author usage is deprecated in v0.1
  - single author usage is removed in API v2
* Splitted posts & postsPublic controller for v2
* Removed requirement to send `status=all` from Admin API v2
* Removed `status` option from pages Content API v2
* Removed `status` options from Users Admin API v2
This commit is contained in:
Katharina Irrgang 2019-02-22 04:17:14 +01:00 committed by Kevin Ansfield
parent cf8622ea99
commit 0a70226128
37 changed files with 1375 additions and 150 deletions

View file

@ -19,6 +19,10 @@ module.exports = {
return require('./session');
},
get pagesPublic() {
return shared.pipeline(require('./pages-public'), localUtils);
},
get pages() {
return shared.pipeline(require('./pages'), localUtils);
},
@ -43,6 +47,10 @@ module.exports = {
return shared.pipeline(require('./posts'), localUtils);
},
get postsPublic() {
return shared.pipeline(require('./posts-public'), localUtils);
},
get invites() {
return shared.pipeline(require('./invites'), localUtils);
},

View file

@ -0,0 +1,73 @@
const common = require('../../lib/common');
const models = require('../../models');
const ALLOWED_INCLUDES = ['tags', 'authors'];
module.exports = {
docName: 'pages',
browse: {
options: [
'include',
'filter',
'fields',
'formats',
'absolute_urls',
'page',
'limit',
'order',
'debug'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
},
formats: {
values: models.Post.allowedFormats
}
}
},
permissions: true,
query(frame) {
return models.Post.findPage(frame.options);
}
},
read: {
options: [
'include',
'fields',
'formats',
'debug',
'absolute_urls'
],
data: [
'id',
'slug',
'uuid'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
},
formats: {
values: models.Post.allowedFormats
}
}
},
permissions: true,
query(frame) {
return models.Post.findOne(frame.data, frame.options)
.then((model) => {
if (!model) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.pages.pageNotFound')
});
}
return model;
});
}
}
};

View file

@ -1,6 +1,8 @@
const common = require('../../lib/common');
const models = require('../../models');
const ALLOWED_INCLUDES = ['author', 'tags', 'authors', 'authors.roles'];
const common = require('../../lib/common');
const urlService = require('../../services/url');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const UNSAFE_ATTRS = ['status', 'authors'];
module.exports = {
docName: 'pages',
@ -8,14 +10,13 @@ module.exports = {
options: [
'include',
'filter',
'status',
'fields',
'formats',
'absolute_urls',
'page',
'limit',
'order',
'debug'
'page',
'debug',
'absolute_urls'
],
validation: {
options: {
@ -27,7 +28,10 @@ module.exports = {
}
}
},
permissions: true,
permissions: {
docName: 'posts',
unsafeAttrs: UNSAFE_ATTRS
},
query(frame) {
return models.Post.findPage(frame.options);
}
@ -37,7 +41,6 @@ module.exports = {
options: [
'include',
'fields',
'status',
'formats',
'debug',
'absolute_urls'
@ -45,7 +48,6 @@ module.exports = {
data: [
'id',
'slug',
'status',
'uuid'
],
validation: {
@ -58,7 +60,10 @@ module.exports = {
}
}
},
permissions: true,
permissions: {
docName: 'posts',
unsafeAttrs: UNSAFE_ATTRS
},
query(frame) {
return models.Post.findOne(frame.data, frame.options)
.then((model) => {
@ -71,5 +76,113 @@ module.exports = {
return model;
});
}
},
add: {
statusCode: 201,
headers: {},
options: [
'include'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
}
}
},
permissions: {
docName: 'posts',
unsafeAttrs: UNSAFE_ATTRS
},
query(frame) {
return models.Post.add(frame.data.pages[0], frame.options)
.then((model) => {
if (model.get('status') !== 'published') {
this.headers.cacheInvalidate = false;
} else {
this.headers.cacheInvalidate = true;
}
return model;
});
}
},
edit: {
headers: {},
options: [
'include',
'id'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
},
id: {
required: true
}
}
},
permissions: {
docName: 'posts',
unsafeAttrs: UNSAFE_ATTRS
},
query(frame) {
return models.Post.edit(frame.data.pages[0], frame.options)
.then((model) => {
if (model.get('status') === 'published' && model.wasChanged() ||
model.get('status') === 'draft' && model.previous('status') === 'published') {
this.headers.cacheInvalidate = true;
} else if (model.get('status') === 'draft' && model.previous('status') !== 'published') {
this.headers.cacheInvalidate = {
value: urlService.utils.urlFor({
relativeUrl: urlService.utils.urlJoin('/p', model.get('uuid'), '/')
})
};
} else {
this.headers.cacheInvalidate = false;
}
return model;
});
}
},
destroy: {
statusCode: 204,
headers: {
cacheInvalidate: true
},
options: [
'include',
'id'
],
validation: {
options: {
include: {
values: ALLOWED_INCLUDES
},
id: {
required: true
}
}
},
permissions: {
docName: 'posts',
unsafeAttrs: UNSAFE_ATTRS
},
query(frame) {
frame.options.require = true;
return models.Post.destroy(frame.options)
.return(null)
.catch(models.Post.NotFoundError, () => {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.pages.pageNotFound')
});
});
}
}
};

View file

@ -0,0 +1,73 @@
const models = require('../../models');
const common = require('../../lib/common');
const allowedIncludes = ['tags', 'authors'];
module.exports = {
docName: 'posts',
browse: {
options: [
'include',
'filter',
'fields',
'formats',
'limit',
'order',
'page',
'debug',
'absolute_urls'
],
validation: {
options: {
include: {
values: allowedIncludes
},
formats: {
values: models.Post.allowedFormats
}
}
},
permissions: true,
query(frame) {
return models.Post.findPage(frame.options);
}
},
read: {
options: [
'include',
'fields',
'formats',
'debug',
'absolute_urls'
],
data: [
'id',
'slug',
'uuid'
],
validation: {
options: {
include: {
values: allowedIncludes
},
formats: {
values: models.Post.allowedFormats
}
}
},
permissions: true,
query(frame) {
return models.Post.findOne(frame.data, frame.options)
.then((model) => {
if (!model) {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.posts.postNotFound')
});
}
return model;
});
}
}
};

View file

@ -1,8 +1,8 @@
const models = require('../../models');
const common = require('../../lib/common');
const urlService = require('../../services/url');
const allowedIncludes = ['author', 'tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['author_id', 'status', 'authors'];
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['status', 'authors'];
module.exports = {
docName: 'posts',
@ -12,7 +12,6 @@ module.exports = {
'filter',
'fields',
'formats',
'status',
'limit',
'order',
'page',
@ -41,7 +40,6 @@ module.exports = {
options: [
'include',
'fields',
'status',
'formats',
'debug',
'absolute_urls'
@ -49,7 +47,6 @@ module.exports = {
data: [
'id',
'slug',
'status',
'uuid'
],
validation: {

View file

@ -1,6 +1,6 @@
const common = require('../../lib/common');
const models = require('../../models');
const ALLOWED_INCLUDES = ['author', 'authors', 'tags'];
const ALLOWED_INCLUDES = ['authors', 'tags'];
module.exports = {
docName: 'preview',

View file

@ -14,7 +14,6 @@ module.exports = {
'filter',
'fields',
'limit',
'status',
'order',
'page',
'debug'
@ -42,7 +41,6 @@ module.exports = {
data: [
'id',
'slug',
'status',
'email',
'role'
],

View file

@ -1,5 +1,8 @@
const _ = require('lodash');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:pages');
const converters = require('../../../../../lib/mobiledoc/converters');
const url = require('./utils/url');
const localUtils = require('../../index');
function removeMobiledocFormat(frame) {
if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
@ -40,9 +43,17 @@ module.exports = {
frame.options.filter = 'page:true';
}
removeMobiledocFormat(frame);
if (localUtils.isContentAPI(frame)) {
removeMobiledocFormat(frame);
setDefaultOrder(frame);
}
setDefaultOrder(frame);
if (!localUtils.isContentAPI(frame)) {
// @TODO: remove when we drop v0.1
if (!frame.options.filter || !frame.options.filter.match(/status:/)) {
frame.options.status = 'all';
}
}
debug(frame.options);
},
@ -51,10 +62,52 @@ module.exports = {
debug('read');
frame.data.page = true;
removeMobiledocFormat(frame);
setDefaultOrder(frame);
if (localUtils.isContentAPI(frame)) {
removeMobiledocFormat(frame);
setDefaultOrder(frame);
}
if (!localUtils.isContentAPI(frame)) {
// @TODO: remove when we drop v0.1
if (!frame.options.filter || !frame.options.filter.match(/status:/)) {
frame.data.status = 'all';
}
}
debug(frame.options);
},
add(apiConfig, frame) {
debug('add');
if (_.get(frame,'options.source')) {
const html = frame.data.pages[0].html;
if (frame.options.source === 'html' && !_.isEmpty(html)) {
frame.data.pages[0].mobiledoc = JSON.stringify(converters.htmlToMobiledocConverter(html));
}
}
frame.data.pages[0] = url.forPost(Object.assign({}, frame.data.pages[0]), frame.options);
// @NOTE: force storing page
frame.data.pages[0].page = true;
},
edit(apiConfig, frame) {
this.add(...arguments);
debug('edit');
// @NOTE: force not being able to update a page via pages endpoint
frame.options.page = true;
},
destroy(apiConfig, frame) {
frame.options.destroyBy = {
id: frame.options.id,
page: true
};
}
};

View file

@ -1,7 +1,7 @@
const _ = require('lodash');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:posts');
const url = require('./utils/url');
const utils = require('../../index');
const localUtils = require('../../index');
const labs = require('../../../../../services/labs');
const converters = require('../../../../../lib/mobiledoc/converters');
@ -38,27 +38,27 @@ module.exports = {
browse(apiConfig, frame) {
debug('browse');
/**
* CASE:
*
* - posts endpoint only returns posts, not pages
* - we have to enforce the filter
*
* @TODO: https://github.com/TryGhost/Ghost/issues/10268
*/
if (frame.options.filter) {
frame.options.filter = `(${frame.options.filter})+page:false`;
} else {
frame.options.filter = 'page:false';
}
/**
* ## current cases:
* - context object is empty (functional call, content api access)
* - api_key.type == 'content' ? content api access
* - user exists? admin api access
*/
if (utils.isContentAPI(frame)) {
/**
* CASE:
*
* - the content api endpoints for posts should only return non page type resources
* - we have to enforce the filter
*
* @TODO: https://github.com/TryGhost/Ghost/issues/10268
*/
if (frame.options.filter) {
frame.options.filter = `(${frame.options.filter})+page:false`;
} else {
frame.options.filter = 'page:false';
}
if (localUtils.isContentAPI(frame)) {
// CASE: the content api endpoint for posts should not return mobiledoc
removeMobiledocFormat(frame);
@ -70,22 +70,31 @@ module.exports = {
setDefaultOrder(frame);
}
if (!localUtils.isContentAPI(frame)) {
// @TODO: remove when we drop v0.1
if (!frame.options.filter || !frame.options.filter.match(/status:/)) {
frame.options.status = 'all';
}
}
debug(frame.options);
},
read(apiConfig, frame) {
debug('read');
frame.data.page = false;
/**
* ## current cases:
* - context object is empty (functional call, content api access)
* - api_key.type == 'content' ? content api access
* - user exists? admin api access
*/
if (utils.isContentAPI(frame)) {
frame.data.page = false;
if (localUtils.isContentAPI(frame)) {
// CASE: the content api endpoint for posts should not return mobiledoc
removeMobiledocFormat(frame);
if (labs.isSet('members')) {
// CASE: Members needs to have the tags to check if its allowed access
includeTags(frame);
@ -94,20 +103,18 @@ module.exports = {
setDefaultOrder(frame);
}
if (!localUtils.isContentAPI(frame)) {
// @TODO: remove when we drop v0.1
if (!frame.options.filter || !frame.options.filter.match(/status:/)) {
frame.data.status = 'all';
}
}
debug(frame.options);
},
add(apiConfig, frame) {
debug('add');
/**
* Convert author property to author_id to match the name in the database.
*
* @deprecated: `author`, might be removed in Ghost 3.0
*/
if (frame.data.posts[0].hasOwnProperty('author')) {
frame.data.posts[0].author_id = frame.data.posts[0].author;
delete frame.data.posts[0].author;
}
if (_.get(frame,'options.source')) {
const html = frame.data.posts[0].html;
@ -118,9 +125,22 @@ module.exports = {
}
frame.data.posts[0] = url.forPost(Object.assign({}, frame.data.posts[0]), frame.options);
// @NOTE: force storing post
frame.data.posts[0].page = false;
},
edit(apiConfig, frame) {
this.add(apiConfig, frame);
// @NOTE: force that you cannot update pages via posts endpoint
frame.options.page = false;
},
destroy(apiConfig, frame) {
frame.options.destroyBy = {
id: frame.options.id,
page: false
};
}
};

View file

@ -5,6 +5,11 @@ module.exports = {
all(models, apiConfig, frame) {
debug('all');
// CASE: e.g. destroy returns null
if (!models) {
return;
}
if (models.meta) {
frame.response = {
pages: models.data.map(model => mapper.mapPost(model, frame)),

View file

@ -98,6 +98,8 @@ const post = (attrs, frame) => {
if (attrs.og_description === '') {
attrs.og_description = null;
}
} else {
delete attrs.page;
}
delete attrs.locale;

View file

@ -3,6 +3,10 @@ module.exports = {
return require('./posts');
},
get pages() {
return require('./pages');
},
get invites() {
return require('./invites');
},

View file

@ -0,0 +1,37 @@
const Promise = require('bluebird');
const common = require('../../../../../lib/common');
const utils = require('../../index');
const jsonSchema = require('../utils/json-schema');
module.exports = {
add(apiConfig, frame) {
/**
* @NOTE:
*
* Session authentication does not require authors, because the logged in user
* becomes the primary author.
*
* Admin API key requires sending authors, because there is no user id.
*/
if (utils.isAdminAPIKey(frame)) {
if (!frame.data.pages[0].hasOwnProperty('authors')) {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('notices.data.validation.index.validationFailed', {
validationName: 'FieldIsRequired',
key: '"authors"'
})
}));
}
}
const schema = require(`./schemas/pages-add`);
const definitions = require('./schemas/pages');
return jsonSchema.validate(schema, definitions, frame.data);
},
edit(apiConfig, frame) {
const schema = require(`./schemas/pages-edit`);
const definitions = require('./schemas/pages');
return jsonSchema.validate(schema, definitions, frame.data);
}
};

View file

@ -24,24 +24,6 @@ module.exports = {
}
}
/**
* Ensure correct incoming `post.authors` structure.
*
* NOTE:
* The `post.authors[*].id` attribute is required till we release Ghost 3.0.
* Ghost 1.x keeps the deprecated support for `post.author_id`, which is the primary author id and needs to be
* updated if the order of the `post.authors` array changes.
* If we allow adding authors via the post endpoint e.g. `authors=[{name: 'newuser']` (no id property), it's hard
* to update the primary author id (`post.author_id`), because the new author `id` is generated when attaching
* the author to the post. And the attach operation happens in bookshelf-relations, which happens after
* the event handling in the post model.
*
* It's solvable, but not worth right now solving, because the admin UI does not support this feature.
*
* TLDR; You can only attach existing authors to a post.
*
* @TODO: remove `id` restriction in Ghost 3.0
*/
const schema = require(`./schemas/posts-add`);
const definitions = require('./schemas/posts');
return jsonSchema.validate(schema, definitions, frame.data);

View file

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "pages.add",
"title": "pages.add",
"description": "Schema for pages.add",
"type": "object",
"additionalProperties": false,
"properties": {
"pages": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"items": {
"type": "object",
"allOf": [{"$ref": "pages#/definitions/page"}],
"required": ["title"]
}
}
},
"required": ["pages"]
}

View file

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "pages.edit",
"title": "pages.edit",
"description": "Schema for pages.edit",
"type": "object",
"additionalProperties": false,
"properties": {
"pages": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"items": {
"type": "object",
"allOf": [{"$ref": "pages#/definitions/page"}],
"required": ["updated_at"]
}
}
},
"required": ["pages"]
}

View file

@ -0,0 +1,206 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "pages",
"title": "pages",
"description": "Base pages definitions",
"definitions": {
"page": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"maxLength": 2000
},
"slug": {
"type": "string",
"maxLength": 191
},
"mobiledoc": {
"type": ["string", "null"],
"maxLength": 1000000000
},
"html": {
"type": ["string", "null"],
"maxLength": 1000000000
},
"feature_image": {
"type": ["string", "null"],
"format": "uri-reference",
"maxLength": 2000
},
"featured": {
"type": "boolean"
},
"status": {
"type": "string",
"enum": ["published", "draft", "scheduled"]
},
"locale": {
"type": ["string", "null"],
"maxLength": 6
},
"visibility": {
"type": ["string", "null"],
"enum": ["public"]
},
"meta_title": {
"type": ["string", "null"],
"maxLength": 300
},
"meta_description": {
"type": ["string", "null"],
"maxLength": 500
},
"updated_at": {
"type": ["string", "null"],
"format": "date-time"
},
"published_at": {
"type": ["string", "null"],
"format": "date-time"
},
"custom_excerpt": {
"type": ["string", "null"],
"maxLength": 300
},
"codeinjection_head": {
"type": ["string", "null"],
"maxLength": 65535
},
"codeinjection_foot": {
"type": ["string", "null"],
"maxLength": 65535
},
"og_image": {
"type": ["string", "null"],
"format": "uri-reference",
"maxLength": 2000
},
"og_title": {
"type": ["string", "null"],
"maxLength": 300
},
"og_description": {
"type": ["string", "null"],
"maxLength": 500
},
"twitter_image": {
"type": ["string", "null"],
"format": "uri-reference",
"maxLength": 2000
},
"twitter_title": {
"type": ["string", "null"],
"maxLength": 300
},
"twitter_description": {
"type": ["string", "null"],
"maxLength": 500
},
"custom_template": {
"type": ["string", "null"],
"maxLength": 100
},
"authors": {
"$ref": "#/definitions/page-authors"
},
"tags": {
"$ref": "#/definitions/page-tags"
},
"id": {
"strip": true
},
"page": {
"strip": true
},
"author": {
"strip": true
},
"author_id": {
"strip": true
},
"created_at": {
"strip": true
},
"created_by": {
"strip": true
},
"updated_by": {
"strip": true
},
"published_by": {
"strip": true
}
}
},
"page-authors": {
"description": "Authors of the page",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"maxLength": 24
},
"slug": {
"type": "string",
"maxLength": 191
},
"email": {
"type": "string",
"maxLength": 191
},
"roles": {
"strip": true
},
"permissions": {
"strip": true
}
},
"anyOf": [
{"required": ["id"]},
{"required": ["slug"]},
{"required": ["email"]}
]
}
},
"page-tags": {
"description": "Tags of the page",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"maxLength": 24
},
"name": {
"type": "string",
"maxLength": 191
},
"slug": {
"type": ["string", "null"],
"maxLength": 191
},
"parent": {
"strip": true
},
"parent_id": {
"strip": true
},
"pages": {
"strip": true
}
},
"anyOf": [
{"required": ["id"]},
{"required": ["name"]},
{"required": ["slug"]}
]
}
}
}
}

View file

@ -33,9 +33,6 @@
"featured": {
"type": "boolean"
},
"page": {
"type": "boolean"
},
"status": {
"type": "string",
"enum": ["published", "draft", "scheduled"]
@ -115,6 +112,15 @@
"id": {
"strip": true
},
"author": {
"strip": true
},
"author_id": {
"strip": true
},
"page": {
"strip": true
},
"created_at": {
"strip": true
},

View file

@ -17,13 +17,13 @@ var proxy = require('./proxy'),
/**
* v0.1: users, posts, tags
* v2: authors, pages, posts, tagsPublic
* v2: authors, pagesPublic, posts, tagsPublic
*
* @NOTE: if you use "users" in v2, we should fallback to authors
*/
const RESOURCES = {
posts: {
alias: 'posts',
alias: 'postsPublic',
resource: 'posts'
},
tags: {
@ -35,7 +35,7 @@ const RESOURCES = {
resource: 'users'
},
pages: {
alias: 'pages',
alias: 'pagesPublic',
resource: 'posts'
},
authors: {

View file

@ -936,7 +936,14 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
edit: function edit(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'edit');
const id = options.id;
const model = this.forge({id: id});
let model;
if (options.hasOwnProperty('page')) {
model = this.forge({id: id, page: options.page});
delete options.page;
} else {
model = this.forge({id: id});
}
data = this.filterData(data);
@ -945,12 +952,16 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
model.hasTimestamps = false;
}
return model.fetch(options).then(function then(object) {
if (object) {
options.method = 'update';
return object.save(data, options);
}
});
return model
.fetch(options)
.then((object) => {
if (object) {
options.method = 'update';
return object.save(data, options);
}
throw new common.errors.NotFoundError();
});
},
/**
@ -987,6 +998,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
*/
destroy: function destroy(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'destroy');
if (!options.destroyBy) {
options.destroyBy = {
id: options.id

View file

@ -704,7 +704,8 @@ Post = ghostBookshelf.Model.extend({
findOne: ['columns', 'importing', 'withRelated', 'require'],
findPage: ['status', 'staticPages'],
findAll: ['columns', 'filter'],
destroy: ['destroyAll']
destroy: ['destroyAll', 'destroyBy'],
edit: ['page']
};
// The post model additionally supports having a formats option

View file

@ -18,7 +18,7 @@ module.exports.QUERY = {
}
},
post: {
controller: 'posts',
controller: 'postsPublic',
type: 'read',
resource: 'posts',
options: {
@ -26,7 +26,7 @@ module.exports.QUERY = {
}
},
page: {
controller: 'pages',
controller: 'pagesPublic',
type: 'read',
resource: 'pages',
options: {

View file

@ -12,6 +12,7 @@ const notImplemented = function (req, res, next) {
const whitelisted = {
// @NOTE: stable
posts: ['GET', 'PUT', 'DELETE', 'POST'],
pages: ['GET', 'PUT', 'DELETE', 'POST'],
tags: ['GET', 'PUT', 'DELETE', 'POST'],
images: ['POST'],
// @NOTE: experimental

View file

@ -32,6 +32,14 @@ module.exports = function apiRoutes() {
router.put('/posts/:id', mw.authAdminApi, http(apiv2.posts.edit));
router.del('/posts/:id', mw.authAdminApi, http(apiv2.posts.destroy));
// ## Pages
router.get('/pages', mw.authAdminApi, http(apiv2.pages.browse));
router.post('/pages', mw.authAdminApi, http(apiv2.pages.add));
router.get('/pages/:id', mw.authAdminApi, http(apiv2.pages.read));
router.get('/pages/slug/:slug', mw.authAdminApi, http(apiv2.pages.read));
router.put('/pages/:id', mw.authAdminApi, http(apiv2.pages.edit));
router.del('/pages/:id', mw.authAdminApi, http(apiv2.pages.destroy));
// # Integrations
router.get('/integrations', mw.authAdminApi, http(apiv2.integrations.browse));

View file

@ -11,14 +11,14 @@ module.exports = function apiRoutes() {
const http = apiImpl => apiv2.http(apiImpl, 'content');
// ## Posts
router.get('/posts', mw.authenticatePublic, http(apiv2.posts.browse));
router.get('/posts/:id', mw.authenticatePublic, http(apiv2.posts.read));
router.get('/posts/slug/:slug', mw.authenticatePublic, http(apiv2.posts.read));
router.get('/posts', mw.authenticatePublic, http(apiv2.postsPublic.browse));
router.get('/posts/:id', mw.authenticatePublic, http(apiv2.postsPublic.read));
router.get('/posts/slug/:slug', mw.authenticatePublic, http(apiv2.postsPublic.read));
// ## Pages
router.get('/pages', mw.authenticatePublic, http(apiv2.pages.browse));
router.get('/pages/:id', mw.authenticatePublic, http(apiv2.pages.read));
router.get('/pages/slug/:slug', mw.authenticatePublic, http(apiv2.pages.read));
router.get('/pages', mw.authenticatePublic, http(apiv2.pagesPublic.browse));
router.get('/pages/:id', mw.authenticatePublic, http(apiv2.pagesPublic.read));
router.get('/pages/slug/:slug', mw.authenticatePublic, http(apiv2.pagesPublic.read));
// ## Users
router.get('/authors', mw.authenticatePublic, http(apiv2.authors.browse));

View file

@ -102,7 +102,6 @@ describe('DB API', () => {
let jsonResponse = res.body;
let results = jsonResponse.posts;
jsonResponse.posts.should.have.length(7);
_.filter(results, {page: false, status: 'published'}).length.should.equal(7);
});
});
});
@ -118,7 +117,6 @@ describe('DB API', () => {
let jsonResponse = res.body;
let results = jsonResponse.posts;
jsonResponse.posts.should.have.length(7);
_.filter(results, {page: false, status: 'published'}).length.should.equal(7);
})
.then(() => {
return request.delete(localUtils.API.getApiQuery('db/'))

View file

@ -0,0 +1,154 @@
const should = require('should');
const supertest = require('supertest');
const _ = require('lodash');
const ObjectId = require('bson-objectid');
const moment = require('moment-timezone');
const testUtils = require('../../../utils');
const localUtils = require('./utils');
const config = require('../../../../server/config');
const models = require('../../../../server/models');
const ghost = testUtils.startGhost;
let request;
describe('Pages API', function () {
let ghostServer;
let ownerCookie;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'users:extra', 'posts');
})
.then(function (cookie) {
ownerCookie = cookie;
});
});
it('Can retrieve all pages', function (done) {
request.get(localUtils.API.getApiQuery('pages/'))
.set('Origin', config.get('url'))
.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']);
const jsonResponse = res.body;
should.exist(jsonResponse.pages);
localUtils.API.checkResponse(jsonResponse, 'pages');
jsonResponse.pages.should.have.length(2);
localUtils.API.checkResponse(jsonResponse.pages[0], 'page');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.pages[0].featured).should.eql(true);
// Absolute urls by default
jsonResponse.pages[0].url.should.eql(`${config.get('url')}/404/`);
jsonResponse.pages[1].url.should.eql(`${config.get('url')}/static-page-test/`);
done();
});
});
it('Can add a page', function () {
const page = {
title: 'My Page',
page: false,
status: 'published'
};
return request.post(localUtils.API.getApiQuery('pages/'))
.set('Origin', config.get('url'))
.send({pages: [page]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
res.body.pages.length.should.eql(1);
localUtils.API.checkResponse(res.body.pages[0], 'page');
should.exist(res.headers['x-cache-invalidate']);
return models.Post.findOne({
id: res.body.pages[0].id
}, testUtils.context.internal);
})
.then((model) => {
model.get('title').should.eql(page.title);
model.get('status').should.eql(page.status);
model.get('page').should.eql(true);
});
});
it('Can update a page', function () {
const page = {
title: 'updated page',
page: false
};
return request
.get(localUtils.API.getApiQuery(`pages/${testUtils.DataGenerator.Content.posts[5].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
page.updated_at = res.body.pages[0].updated_at;
return request.put(localUtils.API.getApiQuery('pages/' + testUtils.DataGenerator.Content.posts[5].id))
.set('Origin', config.get('url'))
.send({pages: [page]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then((res) => {
should.exist(res.headers['x-cache-invalidate']);
localUtils.API.checkResponse(res.body.pages[0], 'page');
return models.Post.findOne({
id: res.body.pages[0].id
}, testUtils.context.internal);
})
.then((model) => {
model.get('page').should.eql(true);
});
});
it('Cannot get page via posts endpoint', function () {
return request.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[5].id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
it('Cannot update page via posts endpoint', function () {
const page = {
title: 'fails',
updated_at: new Date().toISOString()
};
return request.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[5].id))
.set('Origin', config.get('url'))
.send({posts: [page]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
it('Can delete a page', function () {
return request.del(localUtils.API.getApiQuery('pages/' + testUtils.DataGenerator.Content.posts[5].id))
.set('Origin', config.get('url'))
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204)
.then((res) => {
res.body.should.be.empty();
res.headers['x-cache-invalidate'].should.eql('/*');
});
});
});

View file

@ -43,19 +43,19 @@ describe('Posts API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(11);
jsonResponse.posts.should.have.length(13);
localUtils.API.checkResponse(jsonResponse.posts[0], 'post');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// Ensure default order
jsonResponse.posts[0].slug.should.eql('welcome');
jsonResponse.posts[10].slug.should.eql('html-ipsum');
jsonResponse.posts[0].slug.should.eql('scheduled-post');
jsonResponse.posts[12].slug.should.eql('html-ipsum');
// Absolute urls by default
jsonResponse.posts[0].url.should.eql(`${config.get('url')}/welcome/`);
jsonResponse.posts[9].feature_image.should.eql(`${config.get('url')}/content/images/2018/hey.jpg`);
jsonResponse.posts[0].url.should.eql(`${config.get('url')}/404/`);
jsonResponse.posts[2].url.should.eql(`${config.get('url')}/welcome/`);
jsonResponse.posts[11].feature_image.should.eql(`${config.get('url')}/content/images/2018/hey.jpg`);
done();
});
@ -80,7 +80,6 @@ describe('Posts API', function () {
localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['mobiledoc', 'plaintext'], ['html']);
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// ensure order works
jsonResponse.posts[0].slug.should.eql('apps-integrations');
@ -104,7 +103,7 @@ describe('Posts API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(11);
jsonResponse.posts.should.have.length(13);
localUtils.API.checkResponse(
jsonResponse.posts[0],
'post',
@ -113,17 +112,18 @@ describe('Posts API', function () {
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
jsonResponse.posts[0].tags.length.should.eql(1);
jsonResponse.posts[0].authors.length.should.eql(1);
jsonResponse.posts[0].tags[0].url.should.eql(`${config.get('url')}/tag/getting-started/`);
jsonResponse.posts[0].authors[0].url.should.eql(`${config.get('url')}/author/ghost/`);
jsonResponse.posts[0].tags.length.should.eql(0);
jsonResponse.posts[2].tags.length.should.eql(1);
jsonResponse.posts[2].authors.length.should.eql(1);
jsonResponse.posts[2].tags[0].url.should.eql(`${config.get('url')}/tag/getting-started/`);
jsonResponse.posts[2].authors[0].url.should.eql(`${config.get('url')}/author/ghost/`);
done();
});
});
it('Can filter posts', function (done) {
request.get(localUtils.API.getApiQuery('posts/?filter=page:[false,true]&status=all'))
request.get(localUtils.API.getApiQuery('posts/?filter=featured:true'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
@ -137,13 +137,33 @@ describe('Posts API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(15);
jsonResponse.posts.should.have.length(2);
localUtils.API.checkResponse(jsonResponse.posts[0], 'post');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
done();
});
});
it('Cannot receive pages', function (done) {
request.get(localUtils.API.getApiQuery('posts/?filter=page:true'))
.set('Origin', config.get('url'))
.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']);
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(0);
done();
});
});
it('Can paginate posts', function (done) {
request.get(localUtils.API.getApiQuery('posts/?page=2'))
.set('Origin', config.get('url'))
@ -178,9 +198,9 @@ describe('Posts API', function () {
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse.posts[0], 'post');
jsonResponse.posts[0].id.should.equal(testUtils.DataGenerator.Content.posts[0].id);
jsonResponse.posts[0].page.should.not.be.ok();
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
testUtils.API.isISO8601(jsonResponse.posts[0].created_at).should.be.true();
// Tags aren't included by default
should.not.exist(jsonResponse.posts[0].tags);
@ -205,9 +225,9 @@ describe('Posts API', function () {
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse.posts[0], 'post');
jsonResponse.posts[0].slug.should.equal('welcome');
jsonResponse.posts[0].page.should.not.be.ok();
_.isBoolean(jsonResponse.posts[0].featured).should.eql(true);
_.isBoolean(jsonResponse.posts[0].page).should.eql(true);
// Tags aren't included by default
should.not.exist(jsonResponse.posts[0].tags);
done();
@ -233,8 +253,6 @@ describe('Posts API', function () {
localUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['tags', 'authors']);
jsonResponse.posts[0].page.should.not.be.ok();
jsonResponse.posts[0].authors[0].should.be.an.Object();
localUtils.API.checkResponse(jsonResponse.posts[0].authors[0], 'user', ['url']);
@ -289,7 +307,7 @@ describe('Posts API', function () {
};
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[3].id}/?status=all`))
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[3].id}/`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
@ -313,7 +331,7 @@ describe('Posts API', function () {
};
return request
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[1].id}/?status=all`))
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[1].id}/?`))
.set('Origin', config.get('url'))
.expect(200)
.then((res) => {
@ -344,4 +362,26 @@ describe('Posts API', function () {
res.headers['x-cache-invalidate'].should.eql('/*');
});
});
it('Cannot get post via pages endpoint', function () {
return request.get(localUtils.API.getApiQuery(`pages/${testUtils.DataGenerator.Content.posts[3].id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
it('Cannot update post via pages endpoint', function () {
const post = {
title: 'fails',
updated_at: new Date().toISOString()
};
return request.put(localUtils.API.getApiQuery('pages/' + testUtils.DataGenerator.Content.posts[3].id))
.set('Origin', config.get('url'))
.send({pages: [post]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
});

View file

@ -7,6 +7,7 @@ const API_URL = '/ghost/api/v2/admin/';
const expectedProperties = {
// API top level
posts: ['posts', 'meta'],
pages: ['pages', 'meta'],
tags: ['tags', 'meta'],
users: ['users', 'meta'],
settings: ['settings', 'meta'],
@ -27,9 +28,22 @@ const expectedProperties = {
.without('mobiledoc', 'plaintext')
.without('visibility')
.without('locale')
.without('page')
// always returns computed properties: url, comment_id, primary_tag, primary_author
.without('author_id').concat('url', 'primary_tag', 'primary_author')
,
page: _(schema.posts)
.keys()
// by default we only return html
.without('mobiledoc', 'plaintext')
.without('visibility')
.without('locale')
.without('page')
// always returns computed properties: url, comment_id, primary_tag, primary_author
.without('author_id').concat('url', 'primary_tag', 'primary_author')
,
user: _(schema.users)
.keys()
.without('visibility')

View file

@ -43,7 +43,7 @@ describe('Posts API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(11);
jsonResponse.posts.should.have.length(13);
localUtils.API.checkResponse(
jsonResponse.posts[0],
@ -74,7 +74,8 @@ describe('Posts API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.posts);
localUtils.API.checkResponse(jsonResponse, 'posts');
jsonResponse.posts.should.have.length(11);
jsonResponse.posts.should.have.length(13);
localUtils.API.checkResponse(
jsonResponse.posts[0],
'post',

View file

@ -24,6 +24,7 @@ const expectedProperties = {
.without('mobiledoc', 'plaintext')
.without('visibility')
.without('locale')
.without('page')
// always returns computed properties: url, comment_id, primary_tag, primary_author
.without('author_id').concat('url', 'primary_tag', 'primary_author')
,

View file

@ -35,7 +35,7 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
};
serializers.input.posts.browse(apiConfig, frame);
should.equal(frame.options.filter, undefined);
should.equal(frame.options.filter, 'page:false');
});
it('combine filters', function () {
@ -178,28 +178,7 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
};
serializers.input.posts.read(apiConfig, frame);
should.not.exist(frame.data.page);
});
it('with non public request it does not override data.page', function () {
const apiConfig = {};
const frame = {
apiType: 'admin',
options: {
context: {
api_key: {
id: 1,
type: 'admin'
}
}
},
data: {
page: true
}
};
serializers.input.posts.read(apiConfig, frame);
frame.data.page.should.eql(true);
should.equal(frame.data.page, false);
});
it('remove mobiledoc option from formats', function () {
@ -382,6 +361,7 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
const apiConfig = {};
const mobiledoc = '{"version":"0.3.1","atoms":[],"cards":[],"sections":[]}';
const frame = {
options: {},
data: {
posts: [
{

View file

@ -0,0 +1,393 @@
const _ = require('lodash');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const common = require('../../../../../../../server/lib/common');
const validators = require('../../../../../../../server/api/v2/utils/validators');
describe('Unit: v2/utils/validators/input/pages', function () {
afterEach(function () {
sinon.restore();
});
describe('add', function () {
const apiConfig = {
docName: 'pages'
};
describe('required fields', function () {
it('should fail with no data', function () {
const frame = {
options: {},
data: {}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no pages', function () {
const frame = {
options: {},
data: {
tags: []
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no pages in array', function () {
const frame = {
options: {},
data: {
pages: []
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with more than page', function () {
const frame = {
options: {},
data: {
pages: [],
tags: []
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail without required fields', function () {
const frame = {
options: {},
data: {
pages: [{
what: 'a fail'
}],
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass with required fields', function () {
const frame = {
options: {},
data: {
pages: [{
title: 'pass',
authors: [{id: 'correct'}]
}],
}
};
return validators.input.pages.add(apiConfig, frame);
});
it('should remove `strip`able fields and leave regular fields', function () {
const frame = {
options: {},
data: {
pages: [{
title: 'pass',
authors: [{id: 'correct'}],
id: 'strip me',
created_at: 'strip me',
created_by: 'strip me',
updated_by: 'strip me',
published_by: 'strip me'
}],
}
};
let result = validators.input.pages.add(apiConfig, frame);
should.exist(frame.data.pages[0].title);
should.exist(frame.data.pages[0].authors);
should.not.exist(frame.data.pages[0].id);
should.not.exist(frame.data.pages[0].created_at);
should.not.exist(frame.data.pages[0].created_by);
should.not.exist(frame.data.pages[0].updated_by);
should.not.exist(frame.data.pages[0].published_by);
return result;
});
});
describe('field formats', function () {
const fieldMap = {
title: [123, new Date(), _.repeat('a', 2001)],
slug: [123, new Date(), _.repeat('a', 192)],
mobiledoc: [123, new Date()],
feature_image: [123, new Date(), 'random words'],
featured: [123, new Date(), 'abc'],
status: [123, new Date(), 'abc'],
locale: [123, new Date(), _.repeat('a', 7)],
visibility: [123, new Date(), 'abc'],
meta_title: [123, new Date(), _.repeat('a', 301)],
meta_description: [123, new Date(), _.repeat('a', 501)],
};
Object.keys(fieldMap).forEach(key => {
it(`should fail for bad ${key}`, function () {
const badValues = fieldMap[key];
const checks = badValues.map((value) => {
const page = {};
page[key] = value;
if (key !== 'title') {
page.title = 'abc';
}
const frame = {
options: {},
data: {
pages: [page]
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
return Promise.all(checks);
});
});
});
describe('authors structure', function () {
it('should require properties', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
authors: {}
}
]
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should require id', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
authors: [{
name: 'hey'
}]
}
]
}
};
return validators.input.pages.add(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
authors: [{
id: 'correct',
name: 'ja'
}]
}
]
}
};
return validators.input.pages.add(apiConfig, frame);
});
});
});
describe('edit', function () {
const apiConfig = {
docName: 'pages'
};
describe('required fields', function () {
it('should fail with no data', function () {
const frame = {
options: {},
data: {}
};
return validators.input.pages.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with no pages', function () {
const frame = {
options: {},
data: {
tags: []
}
};
return validators.input.pages.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should fail with more than page', function () {
const frame = {
options: {},
data: {
pages: [],
tags: []
}
};
return validators.input.pages.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass with some fields', function () {
const frame = {
options: {},
data: {
pages: [{
title: 'pass',
updated_at: new Date().toISOString()
}],
}
};
return validators.input.pages.edit(apiConfig, frame);
});
});
describe('authors structure', function () {
it('should require properties', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
authors: {}
}
]
}
};
return validators.input.pages.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should require id', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
authors: [{
name: 'hey'
}]
}
]
}
};
return validators.input.pages.edit(apiConfig, frame)
.then(Promise.reject)
.catch((err) => {
(err instanceof common.errors.ValidationError).should.be.true();
});
});
it('should pass with valid authors', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
updated_at: new Date().toISOString(),
authors: [{
id: 'correct',
name: 'ja'
}]
}
]
}
};
return validators.input.pages.edit(apiConfig, frame);
});
it('should pass without authors', function () {
const frame = {
options: {},
data: {
pages: [
{
title: 'cool',
updated_at: new Date().toISOString()
}
]
}
};
return validators.input.pages.edit(apiConfig, frame);
});
});
});
});

View file

@ -143,7 +143,6 @@ describe('Unit: v2/utils/validators/input/posts', function () {
mobiledoc: [123, new Date()],
feature_image: [123, new Date(), 'random words'],
featured: [123, new Date(), 'abc'],
page: [123, new Date(), 'abc'],
status: [123, new Date(), 'abc'],
locale: [123, new Date(), _.repeat('a', 7)],
visibility: [123, new Date(), 'abc'],

View file

@ -4,6 +4,7 @@ var should = require('should'),
Promise = require('bluebird'),
security = require('../../../../server/lib/security'),
models = require('../../../../server/models'),
common = require('../../../../server/lib/common'),
urlService = require('../../../../server/services/url'),
filters = require('../../../../server/filters'),
testUtils = require('../../../utils');
@ -338,7 +339,7 @@ describe('Models: base', function () {
});
});
it('resolves with nothing and does not call save if no model is fetched', function () {
it('throws an error if model cannot be found on edit', function () {
const data = {
db: 'cooper'
};
@ -354,9 +355,10 @@ describe('Models: base', function () {
.resolves();
const saveSpy = sinon.stub(model, 'save');
return models.Base.Model.edit(data, unfilteredOptions).then((result) => {
should.equal(result, undefined);
should.equal(saveSpy.callCount, 0);
return models.Base.Model.edit(data, unfilteredOptions).then(() => {
throw new Error('That should not happen');
}).catch((err) => {
(err instanceof common.errors.NotFoundError).should.be.true();
});
});
});

View file

@ -273,7 +273,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
describe('static pages', function () {
const routerOptions = {
permalinks: '/:slug/',
query: {controller: 'pages', resource: 'pages'}
query: {controller: 'pagesPublic', resource: 'pages'}
};
let pages;
@ -299,7 +299,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
};
});
sinon.stub(api.v2, 'pages').get(() => {
sinon.stub(api.v2, 'pagesPublic').get(() => {
return {
read: pagesReadStub
};
@ -350,7 +350,7 @@ describe('Unit - services/routing/helpers/entry-lookup', function () {
};
});
sinon.stub(api.v2, 'pages').get(() => {
sinon.stub(api.v2, 'pagesPublic').get(() => {
return {
read: pagesReadStub
};

View file

@ -1288,7 +1288,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
home: {
controller: 'pages',
controller: 'pagesPublic',
resource: 'pages',
type: 'read',
options: {
@ -1399,7 +1399,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
food: {
controller: 'posts',
controller: 'postsPublic',
resource: 'posts',
type: 'browse',
options: {}
@ -1415,7 +1415,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
posts: {
controller: 'posts',
controller: 'postsPublic',
resource: 'posts',
type: 'read',
options: {
@ -1454,7 +1454,7 @@ describe('UNIT: services/settings/validate', function () {
data: {
query: {
gym: {
controller: 'posts',
controller: 'postsPublic',
resource: 'posts',
type: 'read',
options: {