0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Dynamic Routing Beta: Added yaml validation for data key

refs #9601

- support short and long form of data key
- always return the expanded version (long) to the routers
This commit is contained in:
kirrg001 2018-06-24 00:32:02 +02:00 committed by Katharina Irrgang
parent 4280fa3c58
commit 935f064357
2 changed files with 470 additions and 0 deletions

View file

@ -1,5 +1,6 @@
const _ = require('lodash'); const _ = require('lodash');
const common = require('../../lib/common'); const common = require('../../lib/common');
const RESOURCE_CONFIG = require('../../services/routing/assets/resource-config');
const _private = {}; const _private = {};
_private.validateTemplate = function validateTemplate(object) { _private.validateTemplate = function validateTemplate(object) {
@ -25,6 +26,138 @@ _private.validateTemplate = function validateTemplate(object) {
return object; return object;
}; };
_private.validateData = function validateData(object) {
if (!object.hasOwnProperty('data')) {
return object;
}
const shortToLongForm = (shortForm, options = {}) => {
let longForm = {
query: {},
router: {}
};
if (!shortForm.match(/.*\..*/)) {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.services.settings.yaml.validate', {
at: shortForm,
reason: 'Incorrect Format. Please use e.g. tag.recipes'
})
});
}
let redirect = false;
// CASE: user wants to redirect traffic from resource to route
// @TODO: enable redirect feature if confirmed
if (false && shortForm.match(/^->/)) { // eslint-disable-line no-constant-condition
shortForm = shortForm.replace(/^->/, '');
redirect = true;
}
let [resourceKey, slug] = shortForm.split('.');
longForm.query[options.resourceKey || resourceKey] = {};
longForm.query[options.resourceKey || resourceKey] = _.omit(_.cloneDeep(RESOURCE_CONFIG.QUERY[resourceKey]), 'alias');
longForm.router = {
[RESOURCE_CONFIG.QUERY[resourceKey].alias]: [{slug: slug, redirect: redirect}]
};
longForm.query[options.resourceKey || resourceKey].options.slug = slug;
return longForm;
};
// CASE: short form e.g. data: tag.recipes (expand to long form)
if (typeof object.data === 'string') {
object.data = shortToLongForm(object.data);
} else {
const requiredQueryFields = ['type', 'resource'];
const allowedQueryValues = {
type: ['read', 'browse'],
resource: _.map(RESOURCE_CONFIG.QUERY, 'resource')
};
const allowedQueryOptions = ['limit', 'filter', 'include', 'slug', 'visibility', 'status'];
const allowedRouterOptions = ['redirect', 'slug'];
const defaultRouterOptions = {
redirect: false
};
let data = {
query: {},
router: {}
};
_.each(object.data, (value, key) => {
// CASE: short form e.g. data: tag.recipes
if (typeof object.data[key] === 'string') {
const longForm = shortToLongForm(object.data[key], {resourceKey: key});
data.query = _.merge(data.query, longForm.query);
_.each(Object.keys(longForm.router), (key) => {
if (data.router[key]) {
data.router[key] = data.router[key].concat(longForm.router[key]);
} else {
data.router[key] = longForm.router[key];
}
});
return;
}
data.query[key] = {
options: {}
};
_.each(requiredQueryFields, (option) => {
if (!object.data[key].hasOwnProperty(option)) {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.services.settings.yaml.validate', {
at: object.data[key],
reason: `${option} is required.`
})
});
}
if (allowedQueryValues[option] && allowedQueryValues[option].indexOf(object.data[key][option]) === -1) {
throw new common.errors.ValidationError({
message: common.i18n.t('errors.services.settings.yaml.validate', {
at: object.data[key][option],
reason: `${object.data[key][option]} not supported.`
})
});
}
data.query[key][option] = object.data[key][option];
});
const DEFAULT_RESOURCE = _.find(RESOURCE_CONFIG.QUERY, {resource: data.query[key].resource});
data.query[key].options = _.pick(object.data[key], allowedQueryOptions);
if (data.query[key].type === 'read') {
data.query[key].options = _.defaults(data.query[key].options, DEFAULT_RESOURCE.options);
}
if (!data.router.hasOwnProperty(DEFAULT_RESOURCE.alias)) {
data.router[DEFAULT_RESOURCE.alias] = [];
}
// CASE: we do not allowed redirects for type browse
if (data.query[key].type === 'read') {
let entry = _.pick(object.data[key], allowedRouterOptions);
entry = _.defaults(entry, defaultRouterOptions);
data.router[DEFAULT_RESOURCE.alias].push(entry);
} else {
data.router[DEFAULT_RESOURCE.alias].push(defaultRouterOptions);
}
});
object.data = data;
}
return object;
};
_private.validateRoutes = function validateRoutes(routes) { _private.validateRoutes = function validateRoutes(routes) {
_.each(routes, (routingTypeObject, routingTypeObjectKey) => { _.each(routes, (routingTypeObject, routingTypeObjectKey) => {
// CASE: we hard-require trailing slashes for the index route // CASE: we hard-require trailing slashes for the index route
@ -59,6 +192,7 @@ _private.validateRoutes = function validateRoutes(routes) {
} }
routes[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject); routes[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject);
routes[routingTypeObjectKey] = _private.validateData(routes[routingTypeObjectKey]);
}); });
return routes; return routes;
@ -145,6 +279,7 @@ _private.validateCollections = function validateCollections(collections) {
} }
collections[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject); collections[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject);
collections[routingTypeObjectKey] = _private.validateData(collections[routingTypeObjectKey]);
}); });
return collections; return collections;

View file

@ -336,4 +336,339 @@ describe('UNIT: services/settings/validate', function () {
}); });
}); });
}); });
describe('data definitions', function () {
it('shortform', function () {
const object = validate({
routes: {
'/food/': {
data: 'tag.food'
},
// @TODO: enable redirect
'/music/': {
data: 'tag.music'
},
'/sleep/': {
data: {
bed: 'tag.bed',
dream: 'tag.dream'
}
}
},
collections: {
'/more/': {
permalink: '/{slug}/',
data: {
home: 'page.home'
}
},
'/podcast/': {
permalink: '/podcast/{slug}/',
data: {
something: 'tag.something'
}
},
'/': {
permalink: '/{slug}/',
data: 'tag.sport'
}
}
});
object.should.eql({
taxonomies: {},
routes: {
'/food/': {
data: {
query: {
tag: {
resource: 'tags',
type: 'read',
options: {
slug: 'food',
visibility: 'public'
}
}
},
router: {
tags: [{redirect: false, slug: 'food'}]
}
},
templates: []
},
'/music/': {
data: {
query: {
tag: {
resource: 'tags',
type: 'read',
options: {
slug: 'music',
visibility: 'public'
}
}
},
router: {
tags: [{redirect: false, slug: 'music'}]
}
},
templates: []
},
'/sleep/': {
data: {
query: {
bed: {
resource: 'tags',
type: 'read',
options: {
slug: 'bed',
visibility: 'public'
}
},
dream: {
resource: 'tags',
type: 'read',
options: {
slug: 'dream',
visibility: 'public'
}
}
},
router: {
tags: [{redirect: false, slug: 'bed'}, {redirect: false, slug: 'dream'}]
}
},
templates: []
}
},
collections: {
'/more/': {
permalink: '/:slug/',
data: {
query: {
home: {
resource: 'posts',
type: 'read',
options: {
page: 1,
slug: 'home',
status: 'published'
}
}
},
router: {
pages: [{redirect: false, slug: 'home'}]
}
},
templates: []
},
'/podcast/': {
permalink: '/podcast/:slug/',
data: {
query: {
something: {
resource: 'tags',
type: 'read',
options: {
slug: 'something',
visibility: 'public'
}
}
},
router: {
tags: [{redirect: false, slug: 'something'}]
}
},
templates: []
},
'/': {
permalink: '/:slug/',
data: {
query: {
tag: {
resource: 'tags',
type: 'read',
options: {
slug: 'sport',
visibility: 'public'
}
}
},
router: {
tags: [{redirect: false, slug: 'sport'}]
}
},
templates: []
}
}
});
});
it('longform', function () {
const object = validate({
routes: {
'/food/': {
data: {
food: {
resource: 'posts',
type: 'browse'
}
}
},
'/wellness/': {
data: {
posts: {
resource: 'posts',
type: 'read',
redirect: true
}
}
},
'/partyparty/': {
data: {
posts: {
resource: 'users',
type: 'read',
slug: 'djgutelaune',
redirect: true
}
}
}
},
collections: {
'/yoga/': {
permalink: '/{slug}/',
data: {
gym: {
resource: 'posts',
type: 'read',
slug: 'ups',
status: 'draft'
}
}
},
}
});
object.should.eql({
taxonomies: {},
routes: {
'/food/': {
data: {
query: {
food: {
resource: 'posts',
type: 'browse',
options: {}
}
},
router: {
posts: [{redirect: false}]
}
},
templates: []
},
'/wellness/': {
data: {
query: {
posts: {
resource: 'posts',
type: 'read',
options: {
status: 'published',
slug: '%s',
page: 0
}
}
},
router: {
posts: [{redirect: true}]
}
},
templates: []
},
'/partyparty/': {
data: {
query: {
posts: {
resource: 'users',
type: 'read',
options: {
slug: 'djgutelaune',
visibility: 'public'
}
}
},
router: {
users: [{redirect: true, slug: 'djgutelaune'}]
}
},
templates: []
}
},
collections: {
'/yoga/': {
permalink: '/:slug/',
data: {
query: {
gym: {
resource: 'posts',
type: 'read',
options: {
page: 0,
slug: 'ups',
status: 'draft'
}
}
},
router: {
posts: [{redirect: false, slug: 'ups'}]
}
},
templates: []
}
}
});
});
it('errors', function () {
try {
validate({
collections: {
'/magic/': {
permalink: '/{slug}/',
data: 'tag:test'
}
}
});
validate({
collections: {
'/magic/': {
permalink: '/{slug}/',
data: {
type: 'edit'
}
}
}
});
validate({
collections: {
'/magic/': {
permalink: '/{slug}/',
data: {
resource: 'subscribers'
}
}
}
});
} catch (err) {
(err instanceof common.errors.ValidationError).should.be.true();
return;
}
throw new Error('should fail');
});
});
}); });