[FIX] Uncessary updates on Settings, Roles and Permissions on startup (#17160)
parent
78c9a31116
commit
b3f2ce7ce8
@ -1,46 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
|
||||
import { CachedCollection } from '../../../ui-cached-collection'; |
||||
import { settings } from '../../lib/settings'; |
||||
|
||||
settings.cachedCollection = new CachedCollection({ |
||||
name: 'public-settings', |
||||
eventType: 'onAll', |
||||
userRelated: false, |
||||
listenChangesForLoggedUsersOnly: true, |
||||
}); |
||||
|
||||
settings.collection = settings.cachedCollection.collection; |
||||
|
||||
settings.dict = new ReactiveDict('settings'); |
||||
|
||||
settings.get = function(_id) { |
||||
return settings.dict.get(_id); |
||||
}; |
||||
|
||||
settings.init = function() { |
||||
let initialLoad = true; |
||||
settings.collection.find().observe({ |
||||
added(record) { |
||||
Meteor.settings[record._id] = record.value; |
||||
settings.dict.set(record._id, record.value); |
||||
settings.load(record._id, record.value, initialLoad); |
||||
}, |
||||
changed(record) { |
||||
Meteor.settings[record._id] = record.value; |
||||
settings.dict.set(record._id, record.value); |
||||
settings.load(record._id, record.value, initialLoad); |
||||
}, |
||||
removed(record) { |
||||
delete Meteor.settings[record._id]; |
||||
settings.dict.set(record._id, null); |
||||
settings.load(record._id, null, initialLoad); |
||||
}, |
||||
}); |
||||
initialLoad = false; |
||||
}; |
||||
|
||||
settings.init(); |
||||
|
||||
export { settings }; |
||||
@ -0,0 +1,50 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveDict } from 'meteor/reactive-dict'; |
||||
|
||||
import { CachedCollection } from '../../../ui-cached-collection'; |
||||
import { SettingsBase, SettingValue } from '../../lib/settings'; |
||||
|
||||
const cachedCollection = new CachedCollection({ |
||||
name: 'public-settings', |
||||
eventType: 'onAll', |
||||
userRelated: false, |
||||
listenChangesForLoggedUsersOnly: true, |
||||
}); |
||||
|
||||
class Settings extends SettingsBase { |
||||
cachedCollection = cachedCollection |
||||
|
||||
collection = cachedCollection.collection; |
||||
|
||||
dict = new ReactiveDict<any>('settings'); |
||||
|
||||
get(_id: string): any { |
||||
return this.dict.get(_id); |
||||
} |
||||
|
||||
init(): void { |
||||
let initialLoad = true; |
||||
this.collection.find().observe({ |
||||
added: (record: {_id: string; value: SettingValue}) => { |
||||
Meteor.settings[record._id] = record.value; |
||||
this.dict.set(record._id, record.value); |
||||
this.load(record._id, record.value, initialLoad); |
||||
}, |
||||
changed: (record: {_id: string; value: SettingValue}) => { |
||||
Meteor.settings[record._id] = record.value; |
||||
this.dict.set(record._id, record.value); |
||||
this.load(record._id, record.value, initialLoad); |
||||
}, |
||||
removed: (record: {_id: string}) => { |
||||
delete Meteor.settings[record._id]; |
||||
this.dict.set(record._id, null); |
||||
this.load(record._id, undefined, initialLoad); |
||||
}, |
||||
}); |
||||
initialLoad = false; |
||||
} |
||||
} |
||||
|
||||
export const settings = new Settings(); |
||||
|
||||
settings.init(); |
||||
@ -1,8 +1,8 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
if (Meteor.isClient) { |
||||
module.exports = require('./client/index.js'); |
||||
module.exports = require('./client/index.ts'); |
||||
} |
||||
if (Meteor.isServer) { |
||||
module.exports = require('./server/index.js'); |
||||
module.exports = require('./server/index.ts'); |
||||
} |
||||
|
||||
@ -1,88 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import _ from 'underscore'; |
||||
|
||||
export const settings = { |
||||
callbacks: {}, |
||||
regexCallbacks: {}, |
||||
ts: new Date(), |
||||
get(_id, callback) { |
||||
if (callback != null) { |
||||
settings.onload(_id, callback); |
||||
if (!Meteor.settings) { |
||||
return; |
||||
} |
||||
if (_id === '*') { |
||||
return Object.keys(Meteor.settings).forEach((key) => { |
||||
const value = Meteor.settings[key]; |
||||
callback(key, value); |
||||
}); |
||||
} |
||||
if (_.isRegExp(_id) && Meteor.settings) { |
||||
return Object.keys(Meteor.settings).forEach((key) => { |
||||
if (!_id.test(key)) { |
||||
return; |
||||
} |
||||
const value = Meteor.settings[key]; |
||||
callback(key, value); |
||||
}); |
||||
} |
||||
return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]); |
||||
} |
||||
if (!Meteor.settings) { |
||||
return; |
||||
} |
||||
if (_.isRegExp(_id)) { |
||||
return Object.keys(Meteor.settings).reduce((items, key) => { |
||||
const value = Meteor.settings[key]; |
||||
if (_id.test(key)) { |
||||
items.push({ |
||||
key, |
||||
value, |
||||
}); |
||||
} |
||||
return items; |
||||
}, []); |
||||
} |
||||
return Meteor.settings && Meteor.settings[_id]; |
||||
}, |
||||
set(_id, value, callback) { |
||||
return Meteor.call('saveSetting', _id, value, callback); |
||||
}, |
||||
batchSet(settings, callback) { |
||||
return Meteor.call('saveSettings', settings, callback); |
||||
}, |
||||
load(key, value, initialLoad) { |
||||
['*', key].forEach((item) => { |
||||
if (settings.callbacks[item]) { |
||||
settings.callbacks[item].forEach((callback) => callback(key, value, initialLoad)); |
||||
} |
||||
}); |
||||
Object.keys(settings.regexCallbacks).forEach((cbKey) => { |
||||
const cbValue = settings.regexCallbacks[cbKey]; |
||||
if (!cbValue.regex.test(key)) { |
||||
return; |
||||
} |
||||
cbValue.callbacks.forEach((callback) => callback(key, value, initialLoad)); |
||||
}); |
||||
}, |
||||
onload(key, callback) { |
||||
// if key is '*'
|
||||
// for key, value in Meteor.settings
|
||||
// callback key, value, false
|
||||
// else if Meteor.settings?[_id]?
|
||||
// callback key, Meteor.settings[_id], false
|
||||
const keys = [].concat(key); |
||||
keys.forEach((k) => { |
||||
if (_.isRegExp(k)) { |
||||
settings.regexCallbacks[name = k.source] = settings.regexCallbacks[name = k.source] || { |
||||
regex: k, |
||||
callbacks: [], |
||||
}; |
||||
settings.regexCallbacks[k.source].callbacks.push(callback); |
||||
} else { |
||||
settings.callbacks[k] = settings.callbacks[k] || []; |
||||
settings.callbacks[k].push(callback); |
||||
} |
||||
}); |
||||
}, |
||||
}; |
||||
@ -0,0 +1,115 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import _ from 'underscore'; |
||||
|
||||
export type SettingValueMultiSelect = Array<{key: string; i18nLabel: string}> |
||||
export type SettingValueRoomPick = Array<{_id: string; name: string}> | string |
||||
export type SettingValue = string | boolean | number | SettingValueMultiSelect | undefined; |
||||
export type SettingComposedValue = {key: string; value: SettingValue}; |
||||
export type SettingCallback = (key: string, value: SettingValue, initialLoad?: boolean) => void; |
||||
|
||||
interface ISettingRegexCallbacks { |
||||
regex: RegExp; |
||||
callbacks: SettingCallback[]; |
||||
} |
||||
|
||||
export class SettingsBase { |
||||
private callbacks = new Map<string, SettingCallback[]>(); |
||||
|
||||
private regexCallbacks = new Map<string, ISettingRegexCallbacks>(); |
||||
|
||||
// private ts = new Date()
|
||||
|
||||
public get(_id: string, callback?: SettingCallback): SettingValue | SettingComposedValue[] | void { |
||||
if (callback != null) { |
||||
this.onload(_id, callback); |
||||
if (!Meteor.settings) { |
||||
return; |
||||
} |
||||
if (_id === '*') { |
||||
return Object.keys(Meteor.settings).forEach((key) => { |
||||
const value = Meteor.settings[key]; |
||||
callback(key, value); |
||||
}); |
||||
} |
||||
if (_.isRegExp(_id) && Meteor.settings) { |
||||
return Object.keys(Meteor.settings).forEach((key) => { |
||||
if (!_id.test(key)) { |
||||
return; |
||||
} |
||||
const value = Meteor.settings[key]; |
||||
callback(key, value); |
||||
}); |
||||
} |
||||
|
||||
return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]); |
||||
} |
||||
|
||||
if (!Meteor.settings) { |
||||
return; |
||||
} |
||||
|
||||
if (_.isRegExp(_id)) { |
||||
return Object.keys(Meteor.settings).reduce((items: SettingComposedValue[], key) => { |
||||
const value = Meteor.settings[key]; |
||||
if (_id.test(key)) { |
||||
items.push({ |
||||
key, |
||||
value, |
||||
}); |
||||
} |
||||
return items; |
||||
}, []); |
||||
} |
||||
|
||||
return Meteor.settings && Meteor.settings[_id]; |
||||
} |
||||
|
||||
set(_id: string, value: SettingValue, callback: () => void): void { |
||||
Meteor.call('saveSetting', _id, value, callback); |
||||
} |
||||
|
||||
batchSet(settings: Array<{_id: string; value: SettingValue}>, callback: () => void): void { |
||||
Meteor.call('saveSettings', settings, callback); |
||||
} |
||||
|
||||
load(key: string, value: SettingValue, initialLoad: boolean): void { |
||||
['*', key].forEach((item) => { |
||||
const callbacks = this.callbacks.get(item); |
||||
if (callbacks) { |
||||
callbacks.forEach((callback) => callback(key, value, initialLoad)); |
||||
} |
||||
}); |
||||
Object.keys(this.regexCallbacks).forEach((cbKey) => { |
||||
const cbValue = this.regexCallbacks.get(cbKey); |
||||
if (!cbValue?.regex.test(key)) { |
||||
return; |
||||
} |
||||
cbValue.callbacks.forEach((callback) => callback(key, value, initialLoad)); |
||||
}); |
||||
} |
||||
|
||||
onload(key: string | string[] | RegExp | RegExp[], callback: SettingCallback): void { |
||||
// if key is '*'
|
||||
// for key, value in Meteor.settings
|
||||
// callback key, value, false
|
||||
// else if Meteor.settings?[_id]?
|
||||
// callback key, Meteor.settings[_id], false
|
||||
const keys: Array<string | RegExp> = Array.isArray(key) ? key : [key]; |
||||
keys.forEach((k) => { |
||||
if (_.isRegExp(k)) { |
||||
if (!this.regexCallbacks.has(k.source)) { |
||||
this.regexCallbacks.set(k.source, { |
||||
regex: k, |
||||
callbacks: [], |
||||
}); |
||||
} |
||||
this.regexCallbacks.get(k.source)?.callbacks.push(callback); |
||||
} else { |
||||
if (!this.callbacks.has(k)) { |
||||
this.callbacks.set(k, []); |
||||
} |
||||
this.callbacks.get(k)?.push(callback); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -1,4 +0,0 @@ |
||||
export namespace settings { |
||||
export function get(name: string, callback?: (key: string, value: any) => void): string; |
||||
export function updateById(_id: string, value: any, editor?: string): number; |
||||
} |
||||
@ -1,309 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { settings } from '../../lib/settings'; |
||||
import Settings from '../../../models/server/models/Settings'; |
||||
|
||||
const blockedSettings = {}; |
||||
|
||||
if (process.env.SETTINGS_BLOCKED) { |
||||
process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => { blockedSettings[settingId] = 1; }); |
||||
} |
||||
|
||||
const hiddenSettings = {}; |
||||
if (process.env.SETTINGS_HIDDEN) { |
||||
process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => { hiddenSettings[settingId] = 1; }); |
||||
} |
||||
|
||||
settings._sorter = {}; |
||||
|
||||
const overrideSetting = (_id, value, options) => { |
||||
if (typeof process !== 'undefined' && process.env && process.env[_id]) { |
||||
value = process.env[_id]; |
||||
if (value.toLowerCase() === 'true') { |
||||
value = true; |
||||
} else if (value.toLowerCase() === 'false') { |
||||
value = false; |
||||
} else if (options.type === 'int') { |
||||
value = parseInt(value); |
||||
} |
||||
options.processEnvValue = value; |
||||
options.valueSource = 'processEnvValue'; |
||||
} else if (Meteor.settings && typeof Meteor.settings[_id] !== 'undefined') { |
||||
if (Meteor.settings[_id] == null) { |
||||
return false; |
||||
} |
||||
|
||||
value = Meteor.settings[_id]; |
||||
options.meteorSettingsValue = value; |
||||
options.valueSource = 'meteorSettingsValue'; |
||||
} |
||||
|
||||
if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { |
||||
let value = process.env[`OVERWRITE_SETTING_${ _id }`]; |
||||
if (value.toLowerCase() === 'true') { |
||||
value = true; |
||||
} else if (value.toLowerCase() === 'false') { |
||||
value = false; |
||||
} else if (options.type === 'int') { |
||||
value = parseInt(value); |
||||
} |
||||
options.value = value; |
||||
options.processEnvValue = value; |
||||
options.valueSource = 'processEnvValue'; |
||||
} |
||||
|
||||
return value; |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Add a setting |
||||
* @param {String} _id |
||||
* @param {Mixed} value |
||||
* @param {Object} setting |
||||
*/ |
||||
|
||||
settings.add = function(_id, value, { editor, ...options } = {}) { |
||||
if (!_id || value == null) { |
||||
return false; |
||||
} |
||||
if (settings._sorter[options.group] == null) { |
||||
settings._sorter[options.group] = 0; |
||||
} |
||||
options.packageValue = value; |
||||
options.valueSource = 'packageValue'; |
||||
options.hidden = options.hidden || false; |
||||
options.blocked = options.blocked || false; |
||||
options.secret = options.secret || false; |
||||
if (options.sorter == null) { |
||||
options.sorter = settings._sorter[options.group]++; |
||||
} |
||||
if (options.enableQuery != null) { |
||||
options.enableQuery = JSON.stringify(options.enableQuery); |
||||
} |
||||
if (options.i18nDefaultQuery != null) { |
||||
options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); |
||||
} |
||||
if (options.i18nLabel == null) { |
||||
options.i18nLabel = _id; |
||||
} |
||||
if (options.i18nDescription == null) { |
||||
options.i18nDescription = `${ _id }_Description`; |
||||
} |
||||
if (blockedSettings[_id] != null) { |
||||
options.blocked = true; |
||||
} |
||||
if (hiddenSettings[_id] != null) { |
||||
options.hidden = true; |
||||
} |
||||
if (options.autocomplete == null) { |
||||
options.autocomplete = true; |
||||
} |
||||
|
||||
value = overrideSetting(_id, value, options); |
||||
|
||||
const updateOperations = { |
||||
$set: options, |
||||
$setOnInsert: { |
||||
createdAt: new Date(), |
||||
}, |
||||
}; |
||||
if (editor != null) { |
||||
updateOperations.$setOnInsert.editor = editor; |
||||
updateOperations.$setOnInsert.packageEditor = editor; |
||||
} |
||||
if (options.value == null) { |
||||
if (options.force === true) { |
||||
updateOperations.$set.value = options.packageValue; |
||||
} else { |
||||
updateOperations.$setOnInsert.value = value; |
||||
} |
||||
} |
||||
const query = _.extend({ |
||||
_id, |
||||
}, updateOperations.$set); |
||||
if (options.section == null) { |
||||
updateOperations.$unset = { |
||||
section: 1, |
||||
}; |
||||
query.section = { |
||||
$exists: false, |
||||
}; |
||||
} |
||||
const existentSetting = Settings.db.findOne(query); |
||||
if (existentSetting != null) { |
||||
if (existentSetting.editor == null && updateOperations.$setOnInsert.editor != null) { |
||||
updateOperations.$set.editor = updateOperations.$setOnInsert.editor; |
||||
delete updateOperations.$setOnInsert.editor; |
||||
} |
||||
} else { |
||||
updateOperations.$set.ts = new Date(); |
||||
} |
||||
return Settings.upsert({ |
||||
_id, |
||||
}, updateOperations); |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Add a setting group |
||||
* @param {String} _id |
||||
*/ |
||||
|
||||
settings.addGroup = function(_id, options = {}, cb) { |
||||
if (!_id) { |
||||
return false; |
||||
} |
||||
if (_.isFunction(options)) { |
||||
cb = options; |
||||
options = {}; |
||||
} |
||||
if (options.i18nLabel == null) { |
||||
options.i18nLabel = _id; |
||||
} |
||||
if (options.i18nDescription == null) { |
||||
options.i18nDescription = `${ _id }_Description`; |
||||
} |
||||
options.ts = new Date(); |
||||
options.blocked = false; |
||||
options.hidden = false; |
||||
if (blockedSettings[_id] != null) { |
||||
options.blocked = true; |
||||
} |
||||
if (hiddenSettings[_id] != null) { |
||||
options.hidden = true; |
||||
} |
||||
Settings.upsert({ |
||||
_id, |
||||
}, { |
||||
$set: options, |
||||
$setOnInsert: { |
||||
type: 'group', |
||||
createdAt: new Date(), |
||||
}, |
||||
}); |
||||
if (cb != null) { |
||||
cb.call({ |
||||
add(id, value, options) { |
||||
if (options == null) { |
||||
options = {}; |
||||
} |
||||
options.group = _id; |
||||
return settings.add(id, value, options); |
||||
}, |
||||
section(section, cb) { |
||||
return cb.call({ |
||||
add(id, value, options) { |
||||
if (options == null) { |
||||
options = {}; |
||||
} |
||||
options.group = _id; |
||||
options.section = section; |
||||
return settings.add(id, value, options); |
||||
}, |
||||
}); |
||||
}, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Remove a setting by id |
||||
* @param {String} _id |
||||
*/ |
||||
|
||||
settings.removeById = function(_id) { |
||||
if (!_id) { |
||||
return false; |
||||
} |
||||
return Settings.removeById(_id); |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Update a setting by id |
||||
* @param {String} _id |
||||
*/ |
||||
|
||||
settings.updateById = function(_id, value, editor) { |
||||
if (!_id || value == null) { |
||||
return false; |
||||
} |
||||
if (editor != null) { |
||||
return Settings.updateValueAndEditorById(_id, value, editor); |
||||
} |
||||
return Settings.updateValueById(_id, value); |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Update options of a setting by id |
||||
* @param {String} _id |
||||
*/ |
||||
|
||||
settings.updateOptionsById = function(_id, options) { |
||||
if (!_id || options == null) { |
||||
return false; |
||||
} |
||||
return Settings.updateOptionsById(_id, options); |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Update a setting by id |
||||
* @param {String} _id |
||||
*/ |
||||
|
||||
settings.clearById = function(_id) { |
||||
if (_id == null) { |
||||
return false; |
||||
} |
||||
return Settings.updateValueById(_id, undefined); |
||||
}; |
||||
|
||||
|
||||
/* |
||||
* Update a setting by id |
||||
*/ |
||||
|
||||
settings.init = function() { |
||||
settings.initialLoad = true; |
||||
Settings.find().observe({ |
||||
added(record) { |
||||
Meteor.settings[record._id] = record.value; |
||||
if (record.env === true) { |
||||
process.env[record._id] = record.value; |
||||
} |
||||
return settings.load(record._id, record.value, settings.initialLoad); |
||||
}, |
||||
changed(record) { |
||||
Meteor.settings[record._id] = record.value; |
||||
if (record.env === true) { |
||||
process.env[record._id] = record.value; |
||||
} |
||||
return settings.load(record._id, record.value, settings.initialLoad); |
||||
}, |
||||
removed(record) { |
||||
delete Meteor.settings[record._id]; |
||||
if (record.env === true) { |
||||
delete process.env[record._id]; |
||||
} |
||||
return settings.load(record._id, undefined, settings.initialLoad); |
||||
}, |
||||
}); |
||||
settings.initialLoad = false; |
||||
settings.afterInitialLoad.forEach((fn) => fn(Meteor.settings)); |
||||
}; |
||||
|
||||
settings.afterInitialLoad = []; |
||||
|
||||
settings.onAfterInitialLoad = function(fn) { |
||||
settings.afterInitialLoad.push(fn); |
||||
if (settings.initialLoad === false) { |
||||
return fn(Meteor.settings); |
||||
} |
||||
}; |
||||
|
||||
export { settings }; |
||||
@ -0,0 +1,33 @@ |
||||
import mock from 'mock-require'; |
||||
|
||||
type Dictionary = { |
||||
[index: string]: any; |
||||
} |
||||
|
||||
class SettingsClass { |
||||
public data = new Map<string, Dictionary>() |
||||
|
||||
public upsertCalls = 0; |
||||
|
||||
findOne(query: Dictionary): any { |
||||
return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => data[key] === value)); |
||||
} |
||||
|
||||
upsert(query: any, update: any): void { |
||||
const existent = this.findOne(query); |
||||
|
||||
const data = { ...existent, ...query, ...update.$set }; |
||||
|
||||
if (!existent) { |
||||
Object.assign(data, update.$setOnInsert); |
||||
} |
||||
|
||||
// console.log(query, data);
|
||||
this.data.set(query._id, data); |
||||
this.upsertCalls++; |
||||
} |
||||
} |
||||
|
||||
export const Settings = new SettingsClass(); |
||||
|
||||
mock('../../../models/server/models/Settings', Settings); |
||||
@ -0,0 +1,319 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
/* eslint-env mocha */ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { expect } from 'chai'; |
||||
|
||||
import { Settings } from './settings.mocks'; |
||||
import { settings } from './settings'; |
||||
|
||||
describe('Settings', () => { |
||||
beforeEach(() => { |
||||
Settings.upsertCalls = 0; |
||||
Settings.data.clear(); |
||||
Meteor.settings = { public: {} }; |
||||
process.env = {}; |
||||
}); |
||||
|
||||
it('should not insert the same setting twice', () => { |
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', true, { |
||||
type: 'boolean', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.be.include({ |
||||
type: 'boolean', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: true, |
||||
value: true, |
||||
valueSource: 'packageValue', |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}); |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', true, { |
||||
type: 'boolean', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' }).value).to.be.equal(true); |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting2', false, { |
||||
type: 'boolean', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expect(Settings.data.size).to.be.equal(3); |
||||
expect(Settings.upsertCalls).to.be.equal(3); |
||||
expect(Settings.findOne({ _id: 'my_setting' }).value).to.be.equal(true); |
||||
expect(Settings.findOne({ _id: 'my_setting2' }).value).to.be.equal(false); |
||||
}); |
||||
|
||||
it('should respect override via environment', () => { |
||||
process.env.OVERWRITE_SETTING_my_setting = '1'; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const expectedSetting = { |
||||
value: 1, |
||||
processEnvValue: 1, |
||||
valueSource: 'processEnvValue', |
||||
type: 'int', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: 0, |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
|
||||
process.env.OVERWRITE_SETTING_my_setting = '2'; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expectedSetting.value = 2; |
||||
expectedSetting.processEnvValue = 2; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(3); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
}); |
||||
|
||||
it('should respect initial value via environment', () => { |
||||
process.env.my_setting = '1'; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const expectedSetting = { |
||||
value: 1, |
||||
processEnvValue: 1, |
||||
valueSource: 'processEnvValue', |
||||
type: 'int', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: 0, |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
|
||||
process.env.my_setting = '2'; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expectedSetting.processEnvValue = 2; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(3); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
}); |
||||
|
||||
it('should respect initial value via Meteor.settings', () => { |
||||
Meteor.settings.my_setting = 1; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const expectedSetting = { |
||||
value: 1, |
||||
meteorSettingsValue: 1, |
||||
valueSource: 'meteorSettingsValue', |
||||
type: 'int', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: 0, |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
|
||||
Meteor.settings.my_setting = 2; |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expectedSetting.meteorSettingsValue = 2; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(3); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
}); |
||||
|
||||
it('should keep original value if value on code was changed', () => { |
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const expectedSetting = { |
||||
value: 0, |
||||
valueSource: 'packageValue', |
||||
type: 'int', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: 0, |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
|
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 1, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expectedSetting.packageValue = 1; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(3); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
}); |
||||
|
||||
it('should change group and section', () => { |
||||
settings.addGroup('group', function() { |
||||
this.section('section', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const expectedSetting = { |
||||
value: 0, |
||||
valueSource: 'packageValue', |
||||
type: 'int', |
||||
sorter: 0, |
||||
group: 'group', |
||||
section: 'section', |
||||
packageValue: 0, |
||||
hidden: false, |
||||
blocked: false, |
||||
secret: false, |
||||
i18nLabel: 'my_setting', |
||||
i18nDescription: 'my_setting_Description', |
||||
autocomplete: true, |
||||
}; |
||||
|
||||
expect(Settings.data.size).to.be.equal(2); |
||||
expect(Settings.upsertCalls).to.be.equal(2); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
|
||||
settings.addGroup('group2', function() { |
||||
this.section('section2', function() { |
||||
this.add('my_setting', 0, { |
||||
type: 'int', |
||||
sorter: 0, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
expectedSetting.group = 'group2'; |
||||
expectedSetting.section = 'section2'; |
||||
|
||||
expect(Settings.data.size).to.be.equal(3); |
||||
expect(Settings.upsertCalls).to.be.equal(4); |
||||
expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,364 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { SettingsBase, SettingValue } from '../../lib/settings'; |
||||
import SettingsModel from '../../../models/server/models/Settings'; |
||||
|
||||
const blockedSettings = new Set<string>(); |
||||
const hiddenSettings = new Set<string>(); |
||||
|
||||
if (process.env.SETTINGS_BLOCKED) { |
||||
process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings.add(settingId.trim())); |
||||
} |
||||
|
||||
if (process.env.SETTINGS_HIDDEN) { |
||||
process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings.add(settingId.trim())); |
||||
} |
||||
|
||||
const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddOptions): SettingValue => { |
||||
const envValue = process.env[_id]; |
||||
if (envValue) { |
||||
if (envValue.toLowerCase() === 'true') { |
||||
value = true; |
||||
} else if (envValue.toLowerCase() === 'false') { |
||||
value = false; |
||||
} else if (options.type === 'int') { |
||||
value = parseInt(envValue); |
||||
} |
||||
options.processEnvValue = value; |
||||
options.valueSource = 'processEnvValue'; |
||||
} else if (typeof Meteor.settings[_id] !== 'undefined') { |
||||
if (Meteor.settings[_id] == null) { |
||||
return false; |
||||
} |
||||
|
||||
value = Meteor.settings[_id]; |
||||
options.meteorSettingsValue = value; |
||||
options.valueSource = 'meteorSettingsValue'; |
||||
} |
||||
|
||||
const overwriteValue = process.env[`OVERWRITE_SETTING_${ _id }`]; |
||||
if (overwriteValue) { |
||||
if (overwriteValue.toLowerCase() === 'true') { |
||||
value = true; |
||||
} else if (overwriteValue.toLowerCase() === 'false') { |
||||
value = false; |
||||
} else if (options.type === 'int') { |
||||
value = parseInt(overwriteValue); |
||||
} |
||||
options.value = value; |
||||
options.processEnvValue = value; |
||||
options.valueSource = 'processEnvValue'; |
||||
} |
||||
|
||||
return value; |
||||
}; |
||||
|
||||
export interface ISettingAddOptions { |
||||
_id?: string; |
||||
type?: 'group' | 'boolean' | 'int' | 'string' | 'asset' | 'code' | 'select' | 'password' | 'action' | 'relativeUrl' | 'language' | 'date' | 'color' | 'font' | 'roomPick' | 'multiSelect'; |
||||
editor?: string; |
||||
packageEditor?: string; |
||||
packageValue?: SettingValue; |
||||
valueSource?: string; |
||||
hidden?: boolean; |
||||
blocked?: boolean; |
||||
secret?: boolean; |
||||
sorter?: number; |
||||
i18nLabel?: string; |
||||
i18nDescription?: string; |
||||
autocomplete?: boolean; |
||||
force?: boolean; |
||||
group?: string; |
||||
section?: string; |
||||
enableQuery?: any; |
||||
processEnvValue?: SettingValue; |
||||
meteorSettingsValue?: SettingValue; |
||||
value?: SettingValue; |
||||
ts?: Date; |
||||
} |
||||
|
||||
export interface ISettingAddGroupOptions { |
||||
hidden?: boolean; |
||||
blocked?: boolean; |
||||
ts?: Date; |
||||
i18nLabel?: string; |
||||
i18nDescription?: string; |
||||
} |
||||
|
||||
interface IUpdateOperator { |
||||
$set: ISettingAddOptions; |
||||
$setOnInsert: ISettingAddOptions & { |
||||
createdAt: Date; |
||||
}; |
||||
$unset?: { |
||||
section?: 1; |
||||
}; |
||||
} |
||||
|
||||
type QueryExpression = { |
||||
$exists: boolean; |
||||
} |
||||
|
||||
type Query<T> = { |
||||
[P in keyof T]?: T[P] | QueryExpression; |
||||
} |
||||
|
||||
type addSectionCallback = (this: { |
||||
add(id: string, value: SettingValue, options: ISettingAddOptions): void; |
||||
}) => void; |
||||
|
||||
type addGroupCallback = (this: { |
||||
add(id: string, value: SettingValue, options: ISettingAddOptions): void; |
||||
section(section: string, cb: addSectionCallback): void; |
||||
}) => void; |
||||
|
||||
class Settings extends SettingsBase { |
||||
private afterInitialLoad: Array<(settings: Meteor.Settings) => void> = []; |
||||
|
||||
private _sorter: {[key: string]: number} = {}; |
||||
|
||||
private initialLoad = false; |
||||
|
||||
/* |
||||
* Add a setting |
||||
*/ |
||||
add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean { |
||||
if (!_id || value == null) { |
||||
return false; |
||||
} |
||||
if (options.group && this._sorter[options.group] == null) { |
||||
this._sorter[options.group] = 0; |
||||
} |
||||
options.packageValue = value; |
||||
options.valueSource = 'packageValue'; |
||||
options.hidden = options.hidden || false; |
||||
options.blocked = options.blocked || false; |
||||
options.secret = options.secret || false; |
||||
if (options.group && options.sorter == null) { |
||||
options.sorter = this._sorter[options.group]++; |
||||
} |
||||
if (options.enableQuery != null) { |
||||
options.enableQuery = JSON.stringify(options.enableQuery); |
||||
} |
||||
if (options.i18nLabel == null) { |
||||
options.i18nLabel = _id; |
||||
} |
||||
if (options.i18nDescription == null) { |
||||
options.i18nDescription = `${ _id }_Description`; |
||||
} |
||||
if (blockedSettings.has(_id)) { |
||||
options.blocked = true; |
||||
} |
||||
if (hiddenSettings.has(_id)) { |
||||
options.hidden = true; |
||||
} |
||||
if (options.autocomplete == null) { |
||||
options.autocomplete = true; |
||||
} |
||||
|
||||
value = overrideSetting(_id, value, options); |
||||
|
||||
const updateOperations: IUpdateOperator = { |
||||
$set: options, |
||||
$setOnInsert: { |
||||
createdAt: new Date(), |
||||
}, |
||||
}; |
||||
if (editor != null) { |
||||
updateOperations.$setOnInsert.editor = editor; |
||||
updateOperations.$setOnInsert.packageEditor = editor; |
||||
} |
||||
|
||||
if (options.value == null) { |
||||
if (options.force === true) { |
||||
updateOperations.$set.value = options.packageValue; |
||||
} else { |
||||
updateOperations.$setOnInsert.value = value; |
||||
} |
||||
} |
||||
|
||||
const query: Query<ISettingAddOptions> = { |
||||
_id, |
||||
...updateOperations.$set, |
||||
}; |
||||
|
||||
if (options.section == null) { |
||||
updateOperations.$unset = { |
||||
section: 1, |
||||
}; |
||||
query.section = { |
||||
$exists: false, |
||||
}; |
||||
} |
||||
|
||||
const existentSetting = SettingsModel.findOne(query); |
||||
if (existentSetting) { |
||||
if (existentSetting.editor || !updateOperations.$setOnInsert.editor) { |
||||
return true; |
||||
} |
||||
|
||||
updateOperations.$set.editor = updateOperations.$setOnInsert.editor; |
||||
delete updateOperations.$setOnInsert.editor; |
||||
} |
||||
|
||||
updateOperations.$set.ts = new Date(); |
||||
|
||||
SettingsModel.upsert({ |
||||
_id, |
||||
}, updateOperations); |
||||
return true; |
||||
} |
||||
|
||||
/* |
||||
* Add a setting group |
||||
*/ |
||||
addGroup(_id: string, cb: addGroupCallback): boolean; |
||||
|
||||
// eslint-disable-next-line no-dupe-class-members
|
||||
addGroup(_id: string, options: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): boolean { |
||||
if (!_id) { |
||||
return false; |
||||
} |
||||
if (_.isFunction(options)) { |
||||
cb = options; |
||||
options = {}; |
||||
} |
||||
if (options.i18nLabel == null) { |
||||
options.i18nLabel = _id; |
||||
} |
||||
if (options.i18nDescription == null) { |
||||
options.i18nDescription = `${ _id }_Description`; |
||||
} |
||||
|
||||
options.blocked = false; |
||||
options.hidden = false; |
||||
if (blockedSettings.has(_id)) { |
||||
options.blocked = true; |
||||
} |
||||
if (hiddenSettings.has(_id)) { |
||||
options.hidden = true; |
||||
} |
||||
|
||||
const existentGroup = SettingsModel.findOne({ |
||||
_id, |
||||
type: 'group', |
||||
...options, |
||||
}); |
||||
|
||||
if (!existentGroup) { |
||||
options.ts = new Date(); |
||||
|
||||
SettingsModel.upsert({ |
||||
_id, |
||||
}, { |
||||
$set: options, |
||||
$setOnInsert: { |
||||
type: 'group', |
||||
createdAt: new Date(), |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
if (cb != null) { |
||||
cb.call({ |
||||
add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => { |
||||
options.group = _id; |
||||
return this.add(id, value, options); |
||||
}, |
||||
section: (section: string, cb: addSectionCallback) => cb.call({ |
||||
add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => { |
||||
options.group = _id; |
||||
options.section = section; |
||||
return this.add(id, value, options); |
||||
}, |
||||
}), |
||||
}); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/* |
||||
* Remove a setting by id |
||||
*/ |
||||
removeById(_id: string): boolean { |
||||
if (!_id) { |
||||
return false; |
||||
} |
||||
return SettingsModel.removeById(_id); |
||||
} |
||||
|
||||
/* |
||||
* Update a setting by id |
||||
*/ |
||||
updateById(_id: string, value: SettingValue, editor: string): boolean { |
||||
if (!_id || value == null) { |
||||
return false; |
||||
} |
||||
if (editor != null) { |
||||
return SettingsModel.updateValueAndEditorById(_id, value, editor); |
||||
} |
||||
return SettingsModel.updateValueById(_id, value); |
||||
} |
||||
|
||||
/* |
||||
* Update options of a setting by id |
||||
*/ |
||||
updateOptionsById(_id: string, options: object): boolean { |
||||
if (!_id || options == null) { |
||||
return false; |
||||
} |
||||
return SettingsModel.updateOptionsById(_id, options); |
||||
} |
||||
|
||||
/* |
||||
* Update a setting by id |
||||
*/ |
||||
clearById(_id: string): boolean { |
||||
if (_id == null) { |
||||
return false; |
||||
} |
||||
return SettingsModel.updateValueById(_id, undefined); |
||||
} |
||||
|
||||
/* |
||||
* Update a setting by id |
||||
*/ |
||||
init(): void { |
||||
this.initialLoad = true; |
||||
SettingsModel.find().observe({ |
||||
added: (record: {_id: string; env: boolean; value: SettingValue}) => { |
||||
Meteor.settings[record._id] = record.value; |
||||
if (record.env === true) { |
||||
process.env[record._id] = String(record.value); |
||||
} |
||||
return this.load(record._id, record.value, this.initialLoad); |
||||
}, |
||||
changed: (record: {_id: string; env: boolean; value: SettingValue}) => { |
||||
Meteor.settings[record._id] = record.value; |
||||
if (record.env === true) { |
||||
process.env[record._id] = String(record.value); |
||||
} |
||||
return this.load(record._id, record.value, this.initialLoad); |
||||
}, |
||||
removed: (record: {_id: string; env: boolean}) => { |
||||
delete Meteor.settings[record._id]; |
||||
if (record.env === true) { |
||||
delete process.env[record._id]; |
||||
} |
||||
return this.load(record._id, undefined, this.initialLoad); |
||||
}, |
||||
}); |
||||
this.initialLoad = false; |
||||
this.afterInitialLoad.forEach((fn) => fn(Meteor.settings)); |
||||
} |
||||
|
||||
onAfterInitialLoad(fn: (settings: Meteor.Settings) => void): void { |
||||
this.afterInitialLoad.push(fn); |
||||
if (this.initialLoad === false) { |
||||
fn(Meteor.settings); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const settings = new Settings(); |
||||
@ -0,0 +1,12 @@ |
||||
/* eslint-disable @typescript-eslint/interface-name-prefix */ |
||||
|
||||
declare module 'meteor/reactive-dict' { |
||||
const ReactiveDict: ReactiveDictStatic; |
||||
interface ReactiveDictStatic { |
||||
new <T>(name: string, initialValue?: T): ReactiveDict<T>; |
||||
} |
||||
interface ReactiveDict<T> { |
||||
get(name: string): T; |
||||
set(name: string, newValue: T): void; |
||||
} |
||||
} |
||||
@ -1,4 +1,8 @@ |
||||
--require ts-node/register |
||||
--require babel-mocha-es6-compiler |
||||
--require babel-polyfill |
||||
--reporter spec |
||||
--ui bdd |
||||
--watch-extensions ts |
||||
--extension ts |
||||
app/**/*.tests.js app/**/*.tests.ts |
||||
|
||||
Loading…
Reference in new issue