mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-19 00:11:49 +03:00
00f95e7328
closes #10060
- Implemented scheduling for posts and pages
- Added cache invalidation when scheduling
- Refactored admin token eneration function to accept existing key as parameter in tests
- Added Ghost Scheduler Integration fixture
- Added fixture for permissions for post publish action
- Migrated getScheduled method to v2
- Did not add support for 'from' and 'to' parameters as they were not used by DefaultScheduler
- This method needs rethinking in a long run as it's an ugly hack and should rather become proper endpoint that returns JSON data instead of models
- Removed unused auth middleware from v2 routes
- Added internal scheduler role
- Implemetnted transactions in v2 frame
- This takes into account scenario mentioned in c93f03b87e
- Specifically:
>if two queries happening in a transaction we have to signalise
knex/mysql that we select for an update
otherwise the following case happens:
you fetch posts for an update
a user requests comes in and updates the post (e.g. sets title to "X")
you update the fetched posts, title would get overriden to the old one
131 lines
4.9 KiB
JavaScript
131 lines
4.9 KiB
JavaScript
const _ = require('lodash');
|
|
const moment = require('moment');
|
|
const config = require('../../config');
|
|
const models = require('../../models');
|
|
const urlUtils = require('../../lib/url-utils');
|
|
const common = require('../../lib/common');
|
|
const api = require('./index');
|
|
|
|
module.exports = {
|
|
docName: 'schedules',
|
|
publish: {
|
|
headers: {},
|
|
options: [
|
|
'id',
|
|
'resource'
|
|
],
|
|
data: [
|
|
'force'
|
|
],
|
|
validation: {
|
|
options: {
|
|
id: {
|
|
required: true
|
|
},
|
|
resource: {
|
|
required: true,
|
|
values: ['posts', 'pages']
|
|
}
|
|
}
|
|
},
|
|
permissions: {
|
|
docName: 'posts'
|
|
},
|
|
query(frame) {
|
|
let resource;
|
|
const resourceType = frame.options.resource;
|
|
const publishAPostBySchedulerToleranceInMinutes = config.get('times').publishAPostBySchedulerToleranceInMinutes;
|
|
|
|
return models.Base.transaction((transacting) => {
|
|
const options = {
|
|
transacting: transacting,
|
|
status: 'scheduled',
|
|
forUpdate: true,
|
|
id: frame.options.id,
|
|
context: {
|
|
internal: true
|
|
}
|
|
};
|
|
|
|
return api[resourceType].read({id: frame.options.id}, options)
|
|
.then((result) => {
|
|
resource = result[resourceType][0];
|
|
const publishedAtMoment = moment(resource.published_at);
|
|
|
|
if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) {
|
|
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.job.notFound')}));
|
|
}
|
|
|
|
if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && frame.data.force !== true) {
|
|
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.job.publishInThePast')}));
|
|
}
|
|
|
|
const editedResource = {};
|
|
editedResource[resourceType] = [{
|
|
status: 'published',
|
|
updated_at: moment(resource.updated_at).toISOString(true)
|
|
}];
|
|
|
|
return api[resourceType].edit(
|
|
editedResource,
|
|
_.pick(options, ['context', 'id', 'transacting', 'forUpdate'])
|
|
);
|
|
})
|
|
.then((result) => {
|
|
const scheduledResource = result[resourceType][0];
|
|
|
|
if (
|
|
(scheduledResource.status === 'published' && resource.status !== 'published') ||
|
|
(scheduledResource.status === 'draft' && resource.status === 'published')
|
|
) {
|
|
this.headers.cacheInvalidate = true;
|
|
} else if (
|
|
(scheduledResource.status === 'draft' && resource.status !== 'published') ||
|
|
(scheduledResource.status === 'scheduled' && resource.status !== 'scheduled')
|
|
) {
|
|
this.headers.cacheInvalidate = {
|
|
value: urlUtils.urlFor({
|
|
relativeUrl: urlUtils.urlJoin('/p', scheduledResource.uuid, '/')
|
|
})
|
|
};
|
|
} else {
|
|
this.headers.cacheInvalidate = false;
|
|
}
|
|
|
|
return result;
|
|
});
|
|
});
|
|
}
|
|
},
|
|
|
|
getScheduled: {
|
|
// NOTE: this method is for internal use only by DefaultScheduler
|
|
// it is not exposed anywhere!
|
|
permissions: false,
|
|
validation: {
|
|
options: {
|
|
resource: {
|
|
required: true,
|
|
values: ['posts', 'pages']
|
|
}
|
|
}
|
|
},
|
|
query(frame) {
|
|
const resourceType = frame.options.resource;
|
|
const resourceModel = (resourceType === 'posts') ? 'Post' : 'Page';
|
|
|
|
const cleanOptions = {};
|
|
cleanOptions.filter = 'status:scheduled';
|
|
cleanOptions.columns = ['id', 'published_at', 'created_at'];
|
|
|
|
return models[resourceModel].findAll(cleanOptions)
|
|
.then((result) => {
|
|
let response = {};
|
|
response[resourceType] = result;
|
|
|
|
return response;
|
|
});
|
|
}
|
|
}
|
|
};
|