mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Added initial concept of calculated settings (#14766)
refs: https://github.com/TryGhost/Team/issues/626 - calculated settings are simplified settings (booleans) that are based on other settings or data - they make it easier for us to determine what state features are in elsewhere in ghost e.g. admin and themes - this duplicates some of the members config concepts in the settings service
This commit is contained in:
parent
54b4a3c351
commit
c5ba27e2b5
@ -2,13 +2,23 @@
|
||||
* Settings Lib
|
||||
* A collection of utilities for handling settings including a cache
|
||||
*/
|
||||
const errors = require('@tryghost/errors');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const events = require('../../lib/common/events');
|
||||
const models = require('../../models');
|
||||
const labs = require('../../../shared/labs');
|
||||
const config = require('../../../shared/config');
|
||||
const SettingsCache = require('../../../shared/settings-cache');
|
||||
const SettingsBREADService = require('./settings-bread-service');
|
||||
const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
|
||||
|
||||
const ObjectId = require('bson-objectid');
|
||||
|
||||
const messages = {
|
||||
incorrectKeyType: 'type must be one of "direct" or "connect".'
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {SettingsBREADService} instance of the PostsService
|
||||
*/
|
||||
@ -20,13 +30,36 @@ const getSettingsBREADServiceInstance = () => {
|
||||
});
|
||||
};
|
||||
|
||||
class CalculatedField {
|
||||
constructor({key, type, group, fn, dependents}) {
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
this.group = group;
|
||||
this.fn = fn;
|
||||
this.dependents = dependents;
|
||||
}
|
||||
|
||||
getSetting() {
|
||||
return {
|
||||
key: this.key,
|
||||
type: this.type,
|
||||
group: this.group,
|
||||
value: this.fn(),
|
||||
// @TODO: remove this hack
|
||||
id: ObjectId().toHexString(),
|
||||
created_at: new Date().toISOString().replace(/\d{3}Z$/, '000Z'),
|
||||
updated_at: new Date().toISOString().replace(/\d{3}Z$/, '000Z')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Initialize the cache, used in boot and in testing
|
||||
*/
|
||||
async init() {
|
||||
const settingsCollection = await models.Settings.populateDefaults();
|
||||
SettingsCache.init(events, settingsCollection);
|
||||
SettingsCache.init(events, settingsCollection, this.getCalculatedFields());
|
||||
},
|
||||
|
||||
/**
|
||||
@ -36,6 +69,72 @@ module.exports = {
|
||||
SettingsCache.reset(events);
|
||||
},
|
||||
|
||||
isMembersEnabled() {
|
||||
return SettingsCache.get('members_signup_access') !== 'none';
|
||||
},
|
||||
|
||||
isMembersInviteOnly() {
|
||||
return SettingsCache.get('members_signup_access') === 'invite';
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
|
||||
* @returns {{publicKey: string, secretKey: string} | null}
|
||||
*/
|
||||
getStripeKeys(type) {
|
||||
if (type !== 'direct' && type !== 'connect') {
|
||||
throw new errors.IncorrectUsageError({message: tpl(messages.incorrectKeyType)});
|
||||
}
|
||||
|
||||
const secretKey = SettingsCache.get(`stripe_${type === 'connect' ? 'connect_' : ''}secret_key`);
|
||||
const publicKey = SettingsCache.get(`stripe_${type === 'connect' ? 'connect_' : ''}publishable_key`);
|
||||
|
||||
if (!secretKey || !publicKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
secretKey,
|
||||
publicKey
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {{publicKey: string, secretKey: string} | null}
|
||||
*/
|
||||
getActiveStripeKeys() {
|
||||
const stripeDirect = config.get('stripeDirect');
|
||||
|
||||
if (stripeDirect) {
|
||||
return this.getStripeKeys('direct');
|
||||
}
|
||||
|
||||
const connectKeys = this.getStripeKeys('connect');
|
||||
|
||||
if (!connectKeys) {
|
||||
return this.getStripeKeys('direct');
|
||||
}
|
||||
|
||||
return connectKeys;
|
||||
},
|
||||
|
||||
isStripeConnected() {
|
||||
return this.isMembersEnabled() && this.getActiveStripeKeys() !== null;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getCalculatedFields() {
|
||||
const fields = [];
|
||||
|
||||
fields.push(new CalculatedField({key: 'members_enabled', type: 'boolean', group: 'members', fn: this.isMembersEnabled.bind(this), dependents: ['members_signup_access']}));
|
||||
fields.push(new CalculatedField({key: 'members_invite_only', type: 'boolean', group: 'members', fn: this.isMembersInviteOnly.bind(this), dependents: ['members_signup_access']}));
|
||||
fields.push(new CalculatedField({key: 'paid_members_enabled', type: 'boolean', group: 'members', fn: this.isStripeConnected.bind(this), dependents: ['members_signup_access', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles synchronization of routes.yaml hash loaded in the frontend with
|
||||
* the value stored in the settings table.
|
||||
|
@ -3,6 +3,7 @@
|
||||
// circular dependency bugs.
|
||||
const debug = require('@tryghost/debug')('settings:cache');
|
||||
const _ = require('lodash');
|
||||
|
||||
const publicSettings = require('./public');
|
||||
|
||||
// Local function, only ever used for initializing
|
||||
@ -12,6 +13,13 @@ const updateSettingFromModel = function updateSettingFromModel(settingModel) {
|
||||
module.exports.set(settingModel.get('key'), settingModel.toJSON());
|
||||
};
|
||||
|
||||
const updateCalculatedField = function updateCalculatedField(field) {
|
||||
return () => {
|
||||
debug('Auto updating', field.key);
|
||||
module.exports.set(field.key, field.getSetting());
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Cache
|
||||
* Holds cached settings
|
||||
@ -20,6 +28,7 @@ const updateSettingFromModel = function updateSettingFromModel(settingModel) {
|
||||
* @type {{}} - object of objects
|
||||
*/
|
||||
let settingsCache = {};
|
||||
let _calculatedFields = [];
|
||||
|
||||
const doGet = (key, options) => {
|
||||
if (!settingsCache[key]) {
|
||||
@ -116,10 +125,11 @@ module.exports = {
|
||||
*
|
||||
* @param {EventEmitter} events
|
||||
* @param {Bookshelf.Collection<Settings>} settingsCollection
|
||||
* @param {Array} calculatedFields
|
||||
* @return {object}
|
||||
*/
|
||||
init(events, settingsCollection) {
|
||||
// First, reset the cache and listeners
|
||||
init(events, settingsCollection, calculatedFields) {
|
||||
// First, reset the cache and
|
||||
this.reset(events);
|
||||
|
||||
// // if we have been passed a collection of settings, use this to populate the cache
|
||||
@ -127,11 +137,21 @@ module.exports = {
|
||||
_.each(settingsCollection.models, updateSettingFromModel);
|
||||
}
|
||||
|
||||
_calculatedFields = Array.isArray(calculatedFields) ? calculatedFields : [];
|
||||
|
||||
// Bind to events to automatically keep up-to-date
|
||||
events.on('settings.edited', updateSettingFromModel);
|
||||
events.on('settings.added', updateSettingFromModel);
|
||||
events.on('settings.deleted', updateSettingFromModel);
|
||||
|
||||
// set and bind calculated fields
|
||||
_calculatedFields.forEach((field) => {
|
||||
updateCalculatedField(field)();
|
||||
field.dependents.forEach((dependent) => {
|
||||
events.on(`settings.${dependent}.edited`, updateCalculatedField(field));
|
||||
});
|
||||
});
|
||||
|
||||
return settingsCache;
|
||||
},
|
||||
|
||||
@ -145,5 +165,14 @@ module.exports = {
|
||||
events.removeListener('settings.edited', updateSettingFromModel);
|
||||
events.removeListener('settings.added', updateSettingFromModel);
|
||||
events.removeListener('settings.deleted', updateSettingFromModel);
|
||||
|
||||
//unbind calculated fields
|
||||
_calculatedFields.forEach((field) => {
|
||||
field.dependents.forEach((dependent) => {
|
||||
events.removeListener(`settings.${dependent}.edited`, updateCalculatedField(field));
|
||||
});
|
||||
});
|
||||
|
||||
_calculatedFields = [];
|
||||
}
|
||||
};
|
||||
|
@ -26,5 +26,8 @@ module.exports = {
|
||||
twitter_image: 'twitter_image',
|
||||
twitter_title: 'twitter_title',
|
||||
twitter_description: 'twitter_description',
|
||||
members_support_address: 'members_support_address'
|
||||
members_support_address: 'members_support_address',
|
||||
members_enabled: 'members_enabled',
|
||||
members_invite_only: 'members_invite_only',
|
||||
paid_members_enabled: 'paid_members_enabled'
|
||||
};
|
||||
|
@ -1055,6 +1055,33 @@ Object {
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"value": false,
|
||||
},
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"group": "members",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"key": "members_enabled",
|
||||
"type": "boolean",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"group": "members",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"key": "members_invite_only",
|
||||
"type": "boolean",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"value": false,
|
||||
},
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"group": "members",
|
||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||
"key": "paid_members_enabled",
|
||||
"type": "boolean",
|
||||
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"value": true,
|
||||
},
|
||||
Object {
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"flags": null,
|
||||
@ -1115,7 +1142,7 @@ exports[`Settings API Can request all settings 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "18299",
|
||||
"content-length": "18867",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
|
@ -2,7 +2,7 @@ const assert = require('assert');
|
||||
const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {stringMatching, anyEtag, anyObjectId, anyISODateTime} = matchers;
|
||||
|
||||
const CURRENT_SETTINGS_COUNT = 86;
|
||||
const CURRENT_SETTINGS_COUNT = 89;
|
||||
|
||||
const settingsMatcher = {
|
||||
id: anyObjectId,
|
||||
|
@ -14,6 +14,8 @@ Object {
|
||||
"lang": "en",
|
||||
"locale": "en",
|
||||
"logo": null,
|
||||
"members_enabled": true,
|
||||
"members_invite_only": false,
|
||||
"members_support_address": "noreply",
|
||||
"meta_description": null,
|
||||
"meta_title": null,
|
||||
@ -42,6 +44,7 @@ Object {
|
||||
"og_description": null,
|
||||
"og_image": null,
|
||||
"og_title": null,
|
||||
"paid_members_enabled": true,
|
||||
"secondary_navigation": Array [
|
||||
Object {
|
||||
"label": "Data & privacy",
|
||||
@ -83,9 +86,9 @@ exports[`Settings Content API Can request settings 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "942",
|
||||
"content-length": "1021",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": "W/\\"3ae-FBGPtlUjSvGtTGLOj2sW5Rbn33s\\"",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {anyEtag} = matchers;
|
||||
|
||||
describe('Settings Content API', function () {
|
||||
let agent;
|
||||
@ -12,7 +13,9 @@ describe('Settings Content API', function () {
|
||||
it('Can request settings', async function () {
|
||||
await agent.get('settings/')
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot()
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag
|
||||
})
|
||||
.matchBodySnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -492,6 +492,8 @@ Object {
|
||||
"lang": "en",
|
||||
"locale": "en",
|
||||
"logo": null,
|
||||
"members_enabled": true,
|
||||
"members_invite_only": false,
|
||||
"members_support_address": "noreply",
|
||||
"meta_description": null,
|
||||
"meta_title": null,
|
||||
@ -520,6 +522,7 @@ Object {
|
||||
"og_description": null,
|
||||
"og_image": null,
|
||||
"og_title": null,
|
||||
"paid_members_enabled": true,
|
||||
"secondary_navigation": Array [
|
||||
Object {
|
||||
"label": "Data & privacy",
|
||||
@ -549,10 +552,10 @@ exports[`API Versioning Content API responds with current content version header
|
||||
Object {
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "942",
|
||||
"content-length": "1021",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": "W/\\"3ae-FBGPtlUjSvGtTGLOj2sW5Rbn33s\\"",
|
||||
"etag": "W/\\"3fd-PXBX1gYn1ftAeK+GoVbEnAsmVAE\\"",
|
||||
"vary": "Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
@ -572,6 +575,8 @@ Object {
|
||||
"lang": "en",
|
||||
"locale": "en",
|
||||
"logo": null,
|
||||
"members_enabled": true,
|
||||
"members_invite_only": false,
|
||||
"members_support_address": "noreply",
|
||||
"meta_description": null,
|
||||
"meta_title": null,
|
||||
@ -600,6 +605,7 @@ Object {
|
||||
"og_description": null,
|
||||
"og_image": null,
|
||||
"og_title": null,
|
||||
"paid_members_enabled": true,
|
||||
"secondary_navigation": Array [
|
||||
Object {
|
||||
"label": "Data & privacy",
|
||||
@ -629,9 +635,9 @@ exports[`API Versioning Content API responds with no content version header when
|
||||
Object {
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-length": "942",
|
||||
"content-length": "1021",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"etag": "W/\\"3ae-FBGPtlUjSvGtTGLOj2sW5Rbn33s\\"",
|
||||
"etag": "W/\\"3fd-PXBX1gYn1ftAeK+GoVbEnAsmVAE\\"",
|
||||
"vary": "Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
|
@ -441,6 +441,8 @@ const defaultSettingsKeyTypes = [
|
||||
}
|
||||
];
|
||||
|
||||
const calculatedSettingsTypes = ['members_enabled', 'members_invite_only', 'paid_members_enabled'];
|
||||
|
||||
describe('Settings API (canary)', function () {
|
||||
let request;
|
||||
|
||||
@ -467,7 +469,7 @@ describe('Settings API (canary)', function () {
|
||||
jsonResponse.settings.should.be.an.Object();
|
||||
const settings = jsonResponse.settings;
|
||||
|
||||
should.equal(settings.length, defaultSettingsKeyTypes.length);
|
||||
should.equal(settings.length, (defaultSettingsKeyTypes.length + calculatedSettingsTypes.length));
|
||||
for (const defaultSetting of defaultSettingsKeyTypes) {
|
||||
should.exist(settings.find((setting) => {
|
||||
return (setting.key === defaultSetting.key)
|
||||
@ -500,7 +502,7 @@ describe('Settings API (canary)', function () {
|
||||
jsonResponse.settings.should.be.an.Object();
|
||||
const settings = jsonResponse.settings;
|
||||
// Returns all settings
|
||||
should.equal(settings.length, defaultSettingsKeyTypes.length);
|
||||
should.equal(settings.length, (defaultSettingsKeyTypes.length + calculatedSettingsTypes.length));
|
||||
for (const defaultSetting of defaultSettingsKeyTypes) {
|
||||
should.exist(settings.find((setting) => {
|
||||
return setting.key === defaultSetting.key && setting.type === defaultSetting.type;
|
||||
@ -553,7 +555,7 @@ describe('Settings API (canary)', function () {
|
||||
jsonResponse.settings.should.be.an.Object();
|
||||
const settings = jsonResponse.settings;
|
||||
|
||||
Object.keys(settings).length.should.equal(defaultSettingsKeyTypes.length);
|
||||
Object.keys(settings).length.should.equal((defaultSettingsKeyTypes.length + calculatedSettingsTypes.length));
|
||||
|
||||
localUtils.API.checkResponse(jsonResponse, 'settings');
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ should.equal(true, true);
|
||||
|
||||
describe('UNIT: settings cache', function () {
|
||||
beforeEach(function () {
|
||||
cache.init(events, {});
|
||||
cache.init(events, {}, []);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
Loading…
Reference in New Issue
Block a user