First pass at custom-theme-settings-service functionality

refs https://github.com/TryGhost/Team/issues/1070

- added `bread` util that acts as a wrapper for the provided model, if we have any business functionality needed when settings are added/removed then it will go here
- added primary "server" service that handles syncing of custom theme data extracted from a theme with the settings that are in the database and exported as "Service". Syncing rules on theme activation:
    - if a new setting is seen, create it with the default value
    - if a setting has it's type changed, remove it and create a new setting with the default value
    - if a select setting's value is not a valid option, reset it to the default value
- added shared "frontend/server" service that exposes an in-memory cache of key/value pairs for the currently active theme
This commit is contained in:
Kevin Ansfield 2021-09-22 21:56:45 +01:00
parent 56012f5f21
commit cf9cee0208
6 changed files with 150 additions and 2 deletions

View File

@ -1 +1,4 @@
module.exports = require('./lib/custom-theme-settings-service');
module.exports = {
Service: require('./lib/service'),
Cache: require('./lib/cache')
};

View File

@ -0,0 +1,29 @@
module.exports = class CustomThemeSettingsBREADService {
/**
* @param {Object} options
* @param {Object} options.model - Bookshelf model for custom theme settings
*/
constructor({model}) {
this.Model = model;
}
async browse(data, options = {}) {
return this.Model.findAll(data, options);
}
async read(data, options = {}) {
return this.Model.findOne(data, options);
}
async edit(data, options = {}) {
return this.Model.edit(data, Object.assign({}, options, {method: 'update'}));
}
async add(data, options = {}) {
return this.Model.add(data, options);
}
async destroy(data, options = {}) {
return this.Model.destroy(data, options);
}
};

View File

@ -0,0 +1,43 @@
const BREAD = require('./bread');
const {GhostError} = require('@tryghost/errors');
module.exports = class CustomThemeSettingsCache {
constructor() {
this.content = new Object();
}
init({model}) {
this.repository = new BREAD({model});
}
get(key) {
this._noUsageBeforeInit();
return this.content[key].value;
}
getAll() {
this._noUsageBeforeInit();
return this.content;
}
async populateForTheme(themeName) {
this._noUsageBeforeInit();
const settingsCollection = await this.repository.browse({theme: themeName});
const settingsJson = settingsCollection.toJSON();
this.content = new Object();
settingsJson.forEach((setting) => {
this.content[setting.key] = setting.value;
});
}
_noUsageBeforeInit() {
if (!this.repository) {
throw new GhostError('CustomThemeSettingsCache must have .init({model}) called before being used');
}
}
};

View File

@ -0,0 +1,70 @@
const _ = require('lodash');
const BREAD = require('./bread');
const debug = require('@tryghost/debug')('custom-theme-settings-service');
module.exports = class CustomThemeSettingsService {
constructor({model, cache}) {
this.repository = new BREAD({model});
this.cache = cache;
}
// add/remove/edit theme setting records to match theme settings
async activateTheme(theme) {
const knownSettingsCollection = await this.repository.browse({theme: theme.name});
// convert to JSON so we can use a standard array rather than a bookshelf collection
let knownSettings = knownSettingsCollection.toJSON();
const themeSettings = theme.customSettings || {};
// exit early if there's nothing to sync for this theme
if (knownSettings.length === 0 && _.isEmpty(themeSettings)) {
return;
}
let removedIds = [];
// sync any knownSettings that have changed in the theme
for (const knownSetting of knownSettings) {
const themeSetting = themeSettings[knownSetting.key];
const hasBeenRemoved = !themeSetting;
const hasChangedType = themeSetting && themeSetting.type !== knownSetting.type;
if (hasBeenRemoved || hasChangedType) {
debug(`Removing custom theme setting '${theme.name}.${themeSetting.key}' - ${hasBeenRemoved ? 'not found in theme' : 'type changed'}`);
await this.repository.destroy({id: knownSetting.id});
removedIds.push(knownSetting.id);
return;
}
// replace value with default if it's not a valid select option
if (themeSetting.options && !themeSetting.options.includes(knownSetting.value)) {
debug(`Resetting custom theme setting value '${theme.name}.${themeSetting.key}' - "${knownSetting.value}" is not a valid option`);
await this.repository.edit({value: themeSetting.default}, {id: knownSetting.id});
}
}
// clean up any removed knownSettings now that we've finished looping over them
knownSettings = knownSettings.filter(setting => !removedIds.includes(setting.id));
// add any new settings found in theme (or re-add settings that were removed due to type change)
const knownSettingsKeys = knownSettings.map(setting => setting.key);
for (const [key, setting] of Object.entries(themeSettings)) {
if (!knownSettingsKeys.includes(key)) {
const newSettingValues = {
theme: theme.name,
key,
type: setting.type,
value: setting.default
};
debug(`Adding custom theme setting '${theme.name}.${key}'`);
await this.repository.add(newSettingValues);
}
}
// populate the cache with all key/value pairs for this theme
this.cache.populateForTheme(theme.name);
}
};

View File

@ -24,5 +24,8 @@
"should": "13.2.3",
"sinon": "11.1.2"
},
"dependencies": {}
"dependencies": {
"@tryghost/debug": "^0.1.5",
"@tryghost/errors": "^0.2.14"
}
}