diff --git a/app/api/server/api.js b/app/api/server/api.js index d62382af87551..5a834be36553e 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -374,7 +374,7 @@ export class APIClass extends Restivus { 'error-unauthorized': 'unauthorized', }[e.error] || 'failure'; - result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error, undefined, e); + result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error, process.env.TEST_MODE ? e.stack : undefined, e); } finally { delete Accounts._accountData[connection.id]; } diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index 8e5ac669d2293..69685a3f6f978 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -4,7 +4,7 @@ import { Match, check } from 'meteor/check'; import { Messages } from '../../../models'; import { canAccessRoom, hasPermission } from '../../../authorization'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; -import { processWebhookMessage } from '../../../lib'; +import { processWebhookMessage } from '../../../lib/server'; import { API } from '../api'; import Rooms from '../../../models/server/models/Rooms'; import Users from '../../../models/server/models/Users'; diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.js index c5d790533287f..e74c956625177 100644 --- a/app/api/server/v1/permissions.js +++ b/app/api/server/v1/permissions.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { Permissions, Roles } from '../../../models'; +import { Permissions, Roles } from '../../../models/server'; import { API } from '../api'; /** diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js index 1fd8dba80c0b1..42db479903feb 100644 --- a/app/api/server/v1/settings.js +++ b/app/api/server/v1/settings.js @@ -3,7 +3,7 @@ import { Match, check } from 'meteor/check'; import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { API } from '../api'; diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js index 502ea2537a750..da9fca47fe612 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -24,7 +24,7 @@ class AppServerOrchestrator { initialize() { this._rocketchatLogger = new Logger('Rocket.Chat Apps'); - Permissions.createOrUpdate('manage-apps', ['admin']); + Permissions.create('manage-apps', ['admin']); this._marketplaceUrl = 'https://marketplace.rocket.chat'; diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 16b2f8906580c..0a9dba5a9ff07 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -1,7 +1,7 @@ /* eslint no-multi-spaces: 0 */ import { Meteor } from 'meteor/meteor'; -import { Roles, Permissions, Settings } from '../../models'; +import { Roles, Permissions, Settings } from '../../models/server'; import { settings } from '../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../lib'; import { clearCache } from './functions/hasPermission'; @@ -114,9 +114,7 @@ Meteor.startup(function() { ]; for (const permission of permissions) { - if (!Permissions.findOneById(permission._id)) { - Permissions.upsert(permission._id, { $set: permission }); - } + Permissions.create(permission._id, permission.roles); } const defaultRoles = [ @@ -134,7 +132,7 @@ Meteor.startup(function() { ]; for (const role of defaultRoles) { - Roles.upsert({ _id: role.name }, { $setOnInsert: { scope: role.scope, description: role.description || '', protected: true, mandatory2fa: false } }); + Roles.createOrUpdate(role.name, role.scope, role.description, true, false); } const getPreviousPermissions = function(settingId) { @@ -155,19 +153,17 @@ Meteor.startup(function() { const createSettingPermission = function(setting, previousSettingPermissions) { const permissionId = getSettingPermissionId(setting._id); const permission = { - _id: permissionId, level: CONSTANTS.SETTINGS_LEVEL, // copy those setting-properties which are needed to properly publish the setting-based permissions settingId: setting._id, group: setting.group, section: setting.section, sorter: setting.sorter, + roles: [], }; // copy previously assigned roles if available if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { permission.roles = previousSettingPermissions[permissionId].roles; - } else { - permission.roles = []; } if (setting.group) { permission.groupPermissionId = getSettingPermissionId(setting.group); @@ -175,7 +171,16 @@ Meteor.startup(function() { if (setting.section) { permission.sectionPermissionId = getSettingPermissionId(setting.section); } - Permissions.upsert(permission._id, { $set: permission }); + + const existent = Permissions.findOne({ + _id: permissionId, + ...permission, + }, { fields: { _id: 1 } }); + + if (!existent) { + Permissions.upsert({ _id: permissionId }, { $set: permission }); + } + delete previousSettingPermissions[permissionId]; }; diff --git a/app/channel-settings-mail-messages/server/lib/startup.js b/app/channel-settings-mail-messages/server/lib/startup.js index a04875ad90da4..a079a2c4bc76a 100644 --- a/app/channel-settings-mail-messages/server/lib/startup.js +++ b/app/channel-settings-mail-messages/server/lib/startup.js @@ -7,7 +7,5 @@ Meteor.startup(function() { _id: 'mail-messages', roles: ['admin'], }; - return Permissions.upsert(permission._id, { - $setOnInsert: permission, - }); + return Permissions.create(permission._id, permission.roles); }); diff --git a/app/channel-settings/server/startup.js b/app/channel-settings/server/startup.js index 698723ebe9947..5e9aeb7baaf2f 100644 --- a/app/channel-settings/server/startup.js +++ b/app/channel-settings/server/startup.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Permissions } from '../../models'; Meteor.startup(function() { - Permissions.upsert('post-readonly', { $setOnInsert: { roles: ['admin', 'owner', 'moderator'] } }); - Permissions.upsert('set-readonly', { $setOnInsert: { roles: ['admin', 'owner'] } }); - Permissions.upsert('set-react-when-readonly', { $setOnInsert: { roles: ['admin', 'owner'] } }); + Permissions.create('post-readonly', ['admin', 'owner', 'moderator']); + Permissions.create('set-readonly', ['admin', 'owner']); + Permissions.create('set-react-when-readonly', ['admin', 'owner']); }); diff --git a/app/cloud/server/index.js b/app/cloud/server/index.js index 58bd467b4dfd0..f783f48879235 100644 --- a/app/cloud/server/index.js +++ b/app/cloud/server/index.js @@ -11,7 +11,7 @@ import { Permissions } from '../../models'; import { settings } from '../../settings/server'; if (Permissions) { - Permissions.createOrUpdate('manage-cloud', ['admin']); + Permissions.create('manage-cloud', ['admin']); } const licenseCronName = 'Cloud Workspace Sync'; diff --git a/app/custom-sounds/server/startup/permissions.js b/app/custom-sounds/server/startup/permissions.js index 7a9ba36a56c79..9e66458ea3677 100644 --- a/app/custom-sounds/server/startup/permissions.js +++ b/app/custom-sounds/server/startup/permissions.js @@ -4,6 +4,6 @@ import { Permissions } from '../../../models'; Meteor.startup(() => { if (Permissions) { - Permissions.createOrUpdate('manage-sounds', ['admin']); + Permissions.create('manage-sounds', ['admin']); } }); diff --git a/app/discussion/server/permissions.js b/app/discussion/server/permissions.js index 49bfe798b0b43..2f17e05d94146 100644 --- a/app/discussion/server/permissions.js +++ b/app/discussion/server/permissions.js @@ -10,8 +10,6 @@ Meteor.startup(() => { ]; for (const permission of permissions) { - if (!Permissions.findOneById(permission._id)) { - Permissions.upsert(permission._id, { $set: permission }); - } + Permissions.create(permission._id, permission.roles); } }); diff --git a/app/lib/server/functions/createDirectRoom.js b/app/lib/server/functions/createDirectRoom.js index 93aaa63a4602f..5474e2d674737 100644 --- a/app/lib/server/functions/createDirectRoom.js +++ b/app/lib/server/functions/createDirectRoom.js @@ -1,5 +1,5 @@ import { Rooms, Subscriptions } from '../../../models/server'; -import { settings } from '../../../settings/lib/settings'; +import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/server'; import { callbacks } from '../../../callbacks/server'; diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index 241db8151ea86..4b0c7bfc11357 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -2756,7 +2756,7 @@ settings.addGroup('Setup_Wizard', function() { secret: true, }); - this.add('Cloud_Workspace_Access_Token_Expires_At', new Date(), { + this.add('Cloud_Workspace_Access_Token_Expires_At', new Date(0), { type: 'date', hidden: true, readonly: true, diff --git a/app/mail-messages/server/startup.js b/app/mail-messages/server/startup.js index 10dd89d2b8c00..eadfc796e7762 100644 --- a/app/mail-messages/server/startup.js +++ b/app/mail-messages/server/startup.js @@ -3,10 +3,5 @@ import { Meteor } from 'meteor/meteor'; import { Permissions } from '../../models'; Meteor.startup(function() { - return Permissions.upsert('access-mailer', { - $setOnInsert: { - _id: 'access-mailer', - roles: ['admin'], - }, - }); + return Permissions.create('access-mailer', ['admin']); }); diff --git a/app/message-pin/server/settings.js b/app/message-pin/server/settings.js index c2af7eaf27613..c16f82a183c36 100644 --- a/app/message-pin/server/settings.js +++ b/app/message-pin/server/settings.js @@ -9,9 +9,5 @@ Meteor.startup(function() { group: 'Message', public: true, }); - return Permissions.upsert('pin-message', { - $setOnInsert: { - roles: ['owner', 'moderator', 'admin'], - }, - }); + return Permissions.create('pin-message', ['owner', 'moderator', 'admin']); }); diff --git a/app/message-snippet/server/startup/settings.js b/app/message-snippet/server/startup/settings.js index 04047eb56bfc5..15f8c349e8e62 100644 --- a/app/message-snippet/server/startup/settings.js +++ b/app/message-snippet/server/startup/settings.js @@ -9,9 +9,5 @@ Meteor.startup(function() { public: true, group: 'Message', }); - Permissions.upsert('snippet-message', { - $setOnInsert: { - roles: ['owner', 'moderator', 'admin'], - }, - }); + Permissions.create('snippet-message', ['owner', 'moderator', 'admin']); }); diff --git a/app/models/server/models/Permissions.js b/app/models/server/models/Permissions.js index d1b17ce646989..009f29d37f9df 100644 --- a/app/models/server/models/Permissions.js +++ b/app/models/server/models/Permissions.js @@ -15,15 +15,34 @@ export class Permissions extends Base { } createOrUpdate(name, roles) { + const exists = this.findOne({ + _id: name, + roles, + }, { fields: { _id: 1 } }); + + if (exists) { + return exists._id; + } + + this.upsert({ _id: name }, { $set: { roles } }); + } + + create(name, roles) { + const exists = this.findOneById(name, { fields: { _id: 1 } }); + + if (exists) { + return exists._id; + } + this.upsert({ _id: name }, { $set: { roles } }); } addRole(permission, role) { - this.update({ _id: permission }, { $addToSet: { roles: role } }); + this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); } removeRole(permission, role) { - this.update({ _id: permission }, { $pull: { roles: role } }); + this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); } } diff --git a/app/models/server/models/Roles.js b/app/models/server/models/Roles.js index 2715ab05d7c18..f14c0b9ac15e6 100644 --- a/app/models/server/models/Roles.js +++ b/app/models/server/models/Roles.js @@ -29,21 +29,26 @@ export class Roles extends Base { }); } - createOrUpdate(name, scope = 'Users', description, protectedRole, mandatory2fa) { - const updateData = {}; - updateData.name = name; - updateData.scope = scope; + createOrUpdate(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { + const queryData = { + name, + scope, + protected: protectedRole, + }; - if (description != null) { - updateData.description = description; - } + const updateData = { + ...queryData, + description, + mandatory2fa, + }; - if (protectedRole) { - updateData.protected = protectedRole; - } + const exists = this.findOne({ + _id: name, + ...queryData, + }, { fields: { _id: 1 } }); - if (mandatory2fa != null) { - updateData.mandatory2fa = mandatory2fa; + if (exists) { + return exists._id; } this.upsert({ _id: name }, { $set: updateData }); diff --git a/app/settings/client/index.js b/app/settings/client/index.ts similarity index 100% rename from app/settings/client/index.js rename to app/settings/client/index.ts diff --git a/app/settings/client/lib/settings.js b/app/settings/client/lib/settings.js deleted file mode 100644 index bd824705baec1..0000000000000 --- a/app/settings/client/lib/settings.js +++ /dev/null @@ -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 }; diff --git a/app/settings/client/lib/settings.ts b/app/settings/client/lib/settings.ts new file mode 100644 index 0000000000000..712eadc6c37de --- /dev/null +++ b/app/settings/client/lib/settings.ts @@ -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('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(); diff --git a/app/settings/index.js b/app/settings/index.js index a67eca871efbb..1f3257e45429a 100644 --- a/app/settings/index.js +++ b/app/settings/index.js @@ -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'); } diff --git a/app/settings/lib/settings.js b/app/settings/lib/settings.js deleted file mode 100644 index ab5595f90ac93..0000000000000 --- a/app/settings/lib/settings.js +++ /dev/null @@ -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); - } - }); - }, -}; diff --git a/app/settings/lib/settings.ts b/app/settings/lib/settings.ts new file mode 100644 index 0000000000000..f3aa1f2d0bf5f --- /dev/null +++ b/app/settings/lib/settings.ts @@ -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(); + + private regexCallbacks = new Map(); + + // 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 = 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); + } + }); + } +} diff --git a/app/settings/server/functions/settings.d.ts b/app/settings/server/functions/settings.d.ts deleted file mode 100644 index aab7040d6ccda..0000000000000 --- a/app/settings/server/functions/settings.d.ts +++ /dev/null @@ -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; -} diff --git a/app/settings/server/functions/settings.js b/app/settings/server/functions/settings.js deleted file mode 100644 index 7a7ae4a47c121..0000000000000 --- a/app/settings/server/functions/settings.js +++ /dev/null @@ -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 }; diff --git a/app/settings/server/functions/settings.mocks.ts b/app/settings/server/functions/settings.mocks.ts new file mode 100644 index 0000000000000..16bad40274cb4 --- /dev/null +++ b/app/settings/server/functions/settings.mocks.ts @@ -0,0 +1,33 @@ +import mock from 'mock-require'; + +type Dictionary = { + [index: string]: any; +} + +class SettingsClass { + public data = new Map() + + 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); diff --git a/app/settings/server/functions/settings.tests.ts b/app/settings/server/functions/settings.tests.ts new file mode 100644 index 0000000000000..2cc47778628a3 --- /dev/null +++ b/app/settings/server/functions/settings.tests.ts @@ -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); + }); +}); diff --git a/app/settings/server/functions/settings.ts b/app/settings/server/functions/settings.ts new file mode 100644 index 0000000000000..27a9881b8c36c --- /dev/null +++ b/app/settings/server/functions/settings.ts @@ -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(); +const hiddenSettings = new Set(); + +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 = { + [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 = { + _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(); diff --git a/app/settings/server/index.js b/app/settings/server/index.ts similarity index 100% rename from app/settings/server/index.js rename to app/settings/server/index.ts diff --git a/app/ui-cached-collection/client/models/CachedCollection.js b/app/ui-cached-collection/client/models/CachedCollection.js index 8450ee6b879d5..bcd1ffc5b6bbd 100644 --- a/app/ui-cached-collection/client/models/CachedCollection.js +++ b/app/ui-cached-collection/client/models/CachedCollection.js @@ -120,11 +120,11 @@ const log = (...args) => console.log(`CachedCollection ${ this.name } =>`, ...ar export class CachedCollection extends EventEmitter { constructor({ - collection, + collection = new Mongo.Collection(null), name, - methodName, - syncMethodName, - eventName, + methodName = `${ name }/get`, + syncMethodName = `${ name }/get`, + eventName = `${ name }-changed`, eventType = 'onUser', userRelated = true, listenChangesForLoggedUsersOnly = false, @@ -134,13 +134,13 @@ export class CachedCollection extends EventEmitter { onSyncData = (/* action, record */) => {}, }) { super(); - this.collection = collection || new Mongo.Collection(null); + this.collection = collection; this.ready = new ReactiveVar(false); this.name = name; - this.methodName = methodName || `${ name }/get`; - this.syncMethodName = syncMethodName || `${ name }/get`; - this.eventName = eventName || `${ name }-changed`; + this.methodName = methodName; + this.syncMethodName = syncMethodName; + this.eventName = eventName; this.eventType = eventType; this.useSync = useSync; this.listenChangesForLoggedUsersOnly = listenChangesForLoggedUsersOnly; diff --git a/client/main.d.ts b/client/main.d.ts new file mode 100644 index 0000000000000..fbcc7568aa94d --- /dev/null +++ b/client/main.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable @typescript-eslint/interface-name-prefix */ + +declare module 'meteor/reactive-dict' { + const ReactiveDict: ReactiveDictStatic; + interface ReactiveDictStatic { + new (name: string, initialValue?: T): ReactiveDict; + } + interface ReactiveDict { + get(name: string): T; + set(name: string, newValue: T): void; + } +} diff --git a/ee/app/auditing/server/index.js b/ee/app/auditing/server/index.js index dd531c96949b7..a816e3a1bc2ca 100644 --- a/ee/app/auditing/server/index.js +++ b/ee/app/auditing/server/index.js @@ -19,13 +19,11 @@ onLicense('auditing', () => { ]; permissions.forEach((permission) => { - if (!Permissions.findOneById(permission._id)) { - Permissions.upsert(permission._id, { $set: permission }); - } + Permissions.create(permission._id, permission.roles); }); defaultRoles.forEach((role) => - Roles.upsert({ _id: role.name }, { $setOnInsert: { scope: role.scope, description: role.description || '', protected: true } }), + Roles.createOrUpdate(role.name, role.scope, role.description), ); }); }); diff --git a/ee/app/canned-responses/server/permissions.js b/ee/app/canned-responses/server/permissions.js index 4adec4cd7c13f..5c8100a514d0a 100644 --- a/ee/app/canned-responses/server/permissions.js +++ b/ee/app/canned-responses/server/permissions.js @@ -4,8 +4,8 @@ import { Permissions } from '../../../../app/models'; Meteor.startup(() => { if (Permissions) { - Permissions.createOrUpdate('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); - Permissions.createOrUpdate('save-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); - Permissions.createOrUpdate('remove-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + Permissions.create('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + Permissions.create('save-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); + Permissions.create('remove-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); } }); diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js index c0a0e802842db..ee3b286fe6a20 100644 --- a/ee/app/livechat-enterprise/server/permissions.js +++ b/ee/app/livechat-enterprise/server/permissions.js @@ -39,8 +39,8 @@ export const createPermissions = () => { permissions.map((p) => Permissions.addRole(p, livechatMonitorRole)); - Permissions.createOrUpdate('manage-livechat-units', [adminRole, livechatManagerRole]); - Permissions.createOrUpdate('manage-livechat-monitors', [adminRole, livechatManagerRole]); - Permissions.createOrUpdate('manage-livechat-tags', [adminRole, livechatManagerRole]); - Permissions.createOrUpdate('manage-livechat-priorities', [adminRole, livechatManagerRole]); + Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]); + Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]); + Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]); + Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]); }; diff --git a/mocha.opts b/mocha.opts index 98d12fa40c7bd..a2b834891a7d3 100644 --- a/mocha.opts +++ b/mocha.opts @@ -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 diff --git a/package-lock.json b/package-lock.json index 4732a19cc78b3..053b53b462976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6217,6 +6217,12 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" }, + "@types/chai": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", + "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -6345,6 +6351,21 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" }, + "@types/mocha": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", + "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "dev": true + }, + "@types/mock-require": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-2.0.0.tgz", + "integrity": "sha512-nOgjoE5bBiDeiA+z41i95makyHUSMWQMOPocP+J67Pqx/68HAXaeWN1NFtrAYYV6LrISIZZ8vKHm/a50k0f6Sg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/mongodb": { "version": "3.5.8", "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.8.tgz", @@ -7227,6 +7248,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -13071,6 +13098,12 @@ } } }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -20597,6 +20630,12 @@ "pify": "^3.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "make-plural": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-6.1.0.tgz", @@ -29727,6 +29766,37 @@ "integrity": "sha512-UGTRZu1evMw4uTPyYF66/KFd22XiU+jMaIuHrkIHQ2GivAXVlLV0v/vHrpOuTRf9BmpNHi/SO7Vd0rLu0y57jg==", "dev": true }, + "ts-node": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.8.2.tgz", + "integrity": "sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "3.1.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "ts-pnp": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.5.tgz", @@ -31319,6 +31389,12 @@ } } }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "zip-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz", diff --git a/package.json b/package.json index 3082257f26562..0997ebeb08cbd 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,6 @@ "name": "Rocket.Chat", "url": "https://rocket.chat/" }, - "mocha": { - "tests": [ - "app/**/*.tests.js" - ], - "files": [ - "app/**/*.mocks.js", - "app/**/*.js", - "!app/**/*.tests.js" - ] - }, "keywords": [ "rocketchat", "rocket", @@ -32,11 +22,11 @@ "stylelint": "stylelint \"app/**/*.css\" \"client/**/*.css\" \"app/**/*.less\" \"client/**/*.less\" \"ee/**/*.less\"", "deploy": "npm run build && pm2 startOrRestart pm2.json", "postinstall": "node .scripts/npm-postinstall.js", - "testunit-watch": "mocha --watch --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", - "coverage": "nyc -r html mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", + "coverage": "nyc -r html mocha --opts ./mocha.opts", "test": "node .scripts/start.js", "testui": "cypress run --project tests", - "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", + "testunit": "mocha --opts ./mocha.opts", + "testunit-watch": "mocha --watch --opts ./mocha.opts", "testapi": "mocha --opts ./mocha_api.opts", "testapps": "mocha --opts ./mocha_apps.opts", "testci": "npm run testapi && npm run testapps && npm run testui", @@ -73,7 +63,10 @@ "@storybook/addons": "^5.2.8", "@storybook/react": "^5.2.8", "@types/bcrypt": "^3.0.0", + "@types/chai": "^4.2.11", "@types/meteor": "^1.4.37", + "@types/mocha": "^7.0.2", + "@types/mock-require": "^2.0.0", "@types/mongodb": "^3.5.8", "@typescript-eslint/eslint-plugin": "^2.11.0", "@typescript-eslint/parser": "^2.11.0", @@ -116,6 +109,7 @@ "stylelint": "^9.9.0", "stylelint-order": "^2.0.0", "supertest": "^3.3.0", + "ts-node": "^8.8.2", "typescript": "^3.7.3", "webpack": "^4.29.3" }, diff --git a/tsconfig.json b/tsconfig.json index 3ac6fd928b035..ce035f036be6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "esNext", + "module": "CommonJS", "target": "es2018", "lib": ["esnext", "dom"], @@ -29,7 +29,6 @@ }, "moduleResolution": "node", "resolveJsonModule": true, - "types": ["node"], "esModuleInterop": true, "preserveSymlinks": true,