0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

🐛 Fixed version update indicator on about page

refs https://github.com/TryGhost/Team/issues/754
closes https://github.com/TryGhost/Ghost/issues/13088
refs a7dec233ba

- The corrupted data recovery mechanism for notifications is needed to be able to fix the data stored in `settings` table under `notifications` key. There was no validation in place, which has caused some instances to store data in unreadable/writable state
- The recovery mechanism is in place to avoid adding migrations every time we spot a broken notifications data (will be fixed by validation soon).
- The notification data is also NOT critical but valuable for system functioning properly, that's the reason why the data "healing" happens in less secure  "fire-and-forget" way
- The referenced commit is where the "bigger" problem that was causing the data corruption was at. This change is a "cleanup" after what has happened there - storing Ghost error object in `value` for `notifications` key
This commit is contained in:
Naz 2021-06-24 16:33:01 +04:00
parent e389a6d991
commit b5fb439ae7
3 changed files with 112 additions and 2 deletions

View file

@ -2,5 +2,11 @@ const settingsCache = require('../settings/cache');
const i18n = require('../../../shared/i18n');
const ghostVersion = require('@tryghost/version');
const Notifications = require('./notifications');
const models = require('../../models');
module.exports.notifications = new Notifications({settingsCache, i18n, ghostVersion});
module.exports.notifications = new Notifications({
settingsCache,
i18n,
ghostVersion,
SettingsModel: models.Settings
});

View file

@ -13,16 +13,32 @@ class Notifications {
* @param {Object} options.i18n - i18n instance
* @param {Object} options.ghostVersion
* @param {String} options.ghostVersion.full - Ghost instance version in "full" format - major.minor.patch
* @param {Object} options.SettingsModel - Ghost's Setting model instance
*/
constructor({settingsCache, i18n, ghostVersion}) {
constructor({settingsCache, i18n, ghostVersion, SettingsModel}) {
this.settingsCache = settingsCache;
this.i18n = i18n;
this.ghostVersion = ghostVersion;
this.SettingsModel = SettingsModel;
}
/**
* @returns {Object[]} - all notifications
*/
fetchAllNotifications() {
let allNotifications = this.settingsCache.get('notifications');
// @TODO: this check can be removed to improve read operation perf. It's here only because
// reads are done often and this gives a possibility to self-heal any broken records.
// The check can be removed/moved to write operations once we have the guardrails on that
// level long enough and are confident there's no broken data in the DB (e.g few minors after Ghost v5?)
if (!this.areNotificationsValid(allNotifications)) {
// Not using "await" here and doing the "fire-and-forget" because the result is know beforehand
// We only care for the notifications to ge into "correct" state eventually and work properly with next request
this.dangerousDestroyAll();
return [];
}
allNotifications.forEach((notification) => {
notification.addedAt = moment(notification.addedAt).toDate();
});
@ -30,6 +46,20 @@ class Notifications {
return allNotifications;
}
/**
*
* @param {Object[]} notifications - objects to check if they have valid notifications array format
*
* @returns {boolean}
*/
areNotificationsValid(notifications) {
if (!(_.isArray(notifications))) {
return false;
}
return true;
}
wasSeen(notification, user) {
if (notification.seenBy === undefined) {
return notification.seen;
@ -183,6 +213,22 @@ class Notifications {
return allNotifications;
}
/**
* Comparing to destroyAll method this one wipes out the notifications data!
* It is only to be used in a situation when notifications data has been corrupted and
* there's a need to self-heal. Wiping out notifications will fetch some of the notifications
* again and repopulate the array with correct data.
*/
async dangerousDestroyAll() {
// Same default as defined in "default-settings.json"
const defaultValue = '[]';
return this.SettingsModel.edit([{
key: 'notifications',
value: defaultValue
}], {context: {internal: true}});
}
}
module.exports = Notifications;

View file

@ -153,4 +153,62 @@ describe('Notifications Service', function () {
should.exist(notifications);
notifications.length.should.equal(0);
});
describe('Stored notifications data corruption recovery', function () {
it('should correct broken notifications data on browse', function() {
const settingsCache = {
get: sinon.fake.returns({
message: 'this object should be an array!'
})
};
const settingsModelStub = sinon.stub().resolves();
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: {
full: '5.0.0'
},
SettingsModel: {
edit: settingsModelStub
}
});
const notifications = notificationSvc.browse({user: owner});
should.exist(notifications);
notifications.length.should.equal(0);
settingsModelStub.called.should.equal(true);
settingsModelStub.args[0][0].should.eql([{
key: 'notifications',
value: '[]'
}]);
});
it('does not trigger correction when the data is in valid format', function() {
const settingsCache = {
get: sinon.fake.returns([{
message: 'this works! 5.1.0'
}])
};
const settingsModelStub = sinon.stub().resolves();
const notificationSvc = new Notifications({
settingsCache,
ghostVersion: {
full: '5.0.0'
},
SettingsModel: {
edit: settingsModelStub
}
});
const notifications = notificationSvc.browse({user: owner});
should.exist(notifications);
notifications.length.should.equal(1);
settingsModelStub.called.should.equal(false);
});
});
});