Yuki Takei 4 anni fa
parent
commit
c017b52995

+ 143 - 97
packages/app/src/server/service/config-loader.ts

@@ -1,12 +1,34 @@
-const debug = require('debug')('growi:service:ConfigLoader');
-const { envUtils } = require('growi-commons');
-const isSecurityEnv = require('../../lib/util/isSecurityEnv');
+import { envUtils } from 'growi-commons';
 
+import loggerFactory from '~/utils/logger';
 
-const TYPES = {
-  NUMBER:  { parse: (v) => { return parseInt(v, 10) } },
-  STRING:  { parse: (v) => { return v } },
-  BOOLEAN: { parse: (v) => { return envUtils.toBoolean(v) } },
+import ConfigModel, {
+  Config, defaultCrowiConfigs, defaultMarkdownConfigs, defaultNotificationConfigs,
+} from '../models/config';
+
+const logger = loggerFactory('growi:service:ConfigLoader');
+
+enum ValueType { NUMBER, STRING, BOOLEAN }
+
+interface ValueParser<T> {
+  parse(value: string): T;
+}
+
+interface EnvConfig {
+  ns: string,
+  key: string,
+  type: ValueType,
+  default?: number | string | boolean | null,
+}
+
+type EnumDictionary<T extends string | symbol | number, U> = {
+  [K in T]: U;
+};
+
+const parserDictionary: EnumDictionary<ValueType, ValueParser<number | string | boolean>> = {
+  [ValueType.NUMBER]:  { parse: (v: string) => { return parseInt(v, 10) } },
+  [ValueType.STRING]:  { parse: (v: string) => { return v } },
+  [ValueType.BOOLEAN]: { parse: (v: string) => { return envUtils.toBoolean(v) } },
 };
 
 /**
@@ -26,27 +48,39 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   FILE_UPLOAD: {
     ns:      'crowi',
     key:     'app:fileUploadType',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: 'aws',
   },
   FILE_UPLOAD_USES_ONLY_ENV_VAR_FOR_FILE_UPLOAD_TYPE: {
     ns:      'crowi',
     key:     'app:useOnlyEnvVarForFileUploadType',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
-  // HACKMD_URI: {
-  //   ns:      ,
-  //   key:     ,
-  //   type:    ,
-  //   default:
-  // },
-  // HACKMD_URI_FOR_SERVER: {
-  //   ns:      ,
-  //   key:     ,
-  //   type:    ,
-  //   default:
-  // },
+  HACKMD_URI: {
+    ns:      'crowi',
+    key:     'app:hackmdUri',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  HACKMD_URI_FOR_SERVER: {
+    ns:      'crowi',
+    key:     'app:hackmdUriForServer',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  MATHJAX: {
+    ns:      'crowi',
+    key:     'app:mathJax',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  NO_CDN: {
+    ns:      'crowi',
+    key:     'app:noCdn',
+    type:    ValueType.STRING,
+    default: null,
+  },
   // PLANTUML_URI: {
   //   ns:      ,
   //   key:     ,
@@ -116,121 +150,121 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   DRAWIO_URI: {
     ns:      'crowi',
     key:     'app:drawioUri',
-    type:    TYPES.STRING,
-    default: null,
+    type:    ValueType.STRING,
+    default: 'https://embed.diagrams.net/',
   },
   NCHAN_URI: {
     ns:      'crowi',
     key:     'app:nchanUri',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   APP_SITE_URL: {
     ns:      'crowi',
     key:     'app:siteUrl',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   PUBLISH_OPEN_API: {
     ns:      'crowi',
     key:     'app:publishOpenAPI',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   S2SMSG_PUBSUB_SERVER_TYPE: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:serverType',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   S2SMSG_PUBSUB_NCHAN_PUBLISH_PATH: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:nchan:publishPath',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: '/pubsub',
   },
   S2SMSG_PUBSUB_NCHAN_SUBSCRIBE_PATH: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:nchan:subscribePath',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: '/pubsub',
   },
   S2SMSG_PUBSUB_NCHAN_CHANNEL_ID: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:nchan:channelId',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   S2CMSG_PUBSUB_CONNECTIONS_LIMIT: {
     ns:      'crowi',
     key:     's2cMessagingPubsub:connectionsLimit',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 5000,
   },
   S2CMSG_PUBSUB_CONNECTIONS_LIMIT_FOR_ADMIN: {
     ns:      'crowi',
     key:     's2cMessagingPubsub:connectionsLimitForAdmin',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 100,
   },
   S2CMSG_PUBSUB_CONNECTIONS_LIMIT_FOR_GUEST: {
     ns:      'crowi',
     key:     's2cMessagingPubsub:connectionsLimitForGuest',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 2000,
   },
   MAX_FILE_SIZE: {
     ns:      'crowi',
     key:     'app:maxFileSize',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: Infinity,
   },
   FILE_UPLOAD_TOTAL_LIMIT: {
     ns:      'crowi',
     key:     'app:fileUploadTotalLimit',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: Infinity,
   },
   FILE_UPLOAD_DISABLED: {
     ns:      'crowi',
     key:     'app:fileUploadDisabled',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   FILE_UPLOAD_LOCAL_USE_INTERNAL_REDIRECT: {
     ns:      'crowi',
     key:     'fileUpload:local:useInternalRedirect',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   FILE_UPLOAD_LOCAL_INTERNAL_REDIRECT_PATH: {
     ns:      'crowi',
     key:     'fileUpload:local:internalRedirectPath',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: '/growi-internal/',
   },
   ELASTICSEARCH_URI: {
     ns:      'crowi',
     key:     'app:elasticsearchUri',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   ELASTICSEARCH_REQUEST_TIMEOUT: {
     ns:      'crowi',
     key:     'app:elasticsearchRequestTimeout',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 8000, // msec
   },
   SEARCHBOX_SSL_URL: {
     ns:      'crowi',
     key:     'app:searchboxSslUrl',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   MONGO_GRIDFS_TOTAL_LIMIT: {
     ns:      'crowi',
     key:     'gridfs:totalLimit',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: null, // set null in default for backward compatibility
     //                cz: Newer system respects FILE_UPLOAD_TOTAL_LIMIT.
     //                    If the default value of MONGO_GRIDFS_TOTAL_LIMIT is Infinity,
@@ -239,193 +273,193 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   FORCE_WIKI_MODE: {
     ns:      'crowi',
     key:     'security:wikiMode',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: undefined,
   },
   SESSION_MAX_AGE: {
     ns:      'crowi',
     key:     'security:sessionMaxAge',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: undefined,
   },
   USER_UPPER_LIMIT: {
     ns:      'crowi',
     key:     'security:userUpperLimit',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: Infinity,
   },
   DISABLE_LINK_SHARING: {
     ns:      'crowi',
     key:     'security:disableSharing',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   LOCAL_STRATEGY_ENABLED: {
     ns:      'crowi',
     key:     'security:passport-local:isEnabled',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: true,
   },
   LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     key:     'security:passport-local:useOnlyEnvVarsForSomeOptions',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   SAML_ENABLED: {
     ns:      'crowi',
     key:     'security:passport-saml:isEnabled',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: null,
   },
   SAML_ENTRY_POINT: {
     ns:      'crowi',
     key:     'security:passport-saml:entryPoint',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_CALLBACK_URI: {
     ns:      'crowi',
     key:     'security:passport-saml:callbackUrl',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ISSUER: {
     ns:      'crowi',
     key:     'security:passport-saml:issuer',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ATTR_MAPPING_ID: {
     ns:      'crowi',
     key:     'security:passport-saml:attrMapId',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ATTR_MAPPING_USERNAME: {
     ns:      'crowi',
     key:     'security:passport-saml:attrMapUsername',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ATTR_MAPPING_MAIL: {
     ns:      'crowi',
     key:     'security:passport-saml:attrMapMail',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ATTR_MAPPING_FIRST_NAME: {
     ns:      'crowi',
     key:     'security:passport-saml:attrMapFirstName',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ATTR_MAPPING_LAST_NAME: {
     ns:      'crowi',
     key:     'security:passport-saml:attrMapLastName',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_CERT: {
     ns:      'crowi',
     key:     'security:passport-saml:cert',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SAML_ABLC_RULE: {
     ns:      'crowi',
     key:     'security:passport-saml:ABLCRule',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   S3_REFERENCE_FILE_WITH_RELAY_MODE: {
     ns:      'crowi',
     key:     'aws:referenceFileWithRelayMode',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   S3_LIFETIME_SEC_FOR_TEMPORARY_URL: {
     ns:      'crowi',
     key:     'aws:lifetimeSecForTemporaryUrl',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 120,
   },
   GCS_API_KEY_JSON_PATH: {
     ns:      'crowi',
     key:     'gcs:apiKeyJsonPath',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   GCS_BUCKET: {
     ns:      'crowi',
     key:     'gcs:bucket',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   GCS_UPLOAD_NAMESPACE: {
     ns:      'crowi',
     key:     'gcs:uploadNamespace',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   GCS_REFERENCE_FILE_WITH_RELAY_MODE: {
     ns:      'crowi',
     key:     'gcs:referenceFileWithRelayMode',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   GCS_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     key:     'gcs:useOnlyEnvVarsForSomeOptions',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   GCS_LIFETIME_SEC_FOR_TEMPORARY_URL: {
     ns:      'crowi',
     key:     'gcs:lifetimeSecForTemporaryUrl',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 120,
   },
   PROMSTER_ENABLED: {
     ns:      'crowi',
     key:     'promster:isEnabled',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: false,
   },
   PROMSTER_PORT: {
     ns:      'crowi',
     key:     'promster:port',
-    type:    TYPES.NUMBER,
+    type:    ValueType.NUMBER,
     default: 7788,
   },
   GROWI_CLOUD_URI: {
     ns:      'crowi',
     key:     'app:growiCloudUri',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SLACK_SIGNING_SECRET: {
     ns:      'crowi',
     key:     'slackbot:signingSecret',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SLACK_BOT_TOKEN: {
     ns:      'crowi',
     key:     'slackbot:token',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SLACK_INTEGRATION_PROXY_URI: {
     ns:      'crowi',
     key:     'slackbot:proxyServerUri',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   SLACK_BOT_TYPE: {
@@ -435,35 +469,47 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   GROWI_APP_ID_FOR_GROWI_CLOUD: {
     ns:      'crowi',
     key:     'app:growiAppIdForCloud',
-    type:    TYPES.STRING,
+    type:    ValueType.STRING,
     default: null,
   },
   DEFAULT_EMAIL_PUBLISHED: {
     ns:      'crowi',
     key:     'customize:isEmailPublishedForNewUser',
-    type:    TYPES.BOOLEAN,
+    type:    ValueType.BOOLEAN,
     default: true,
   },
 };
 
-class ConfigLoader {
 
-  constructor(configModel) {
-    this.configModel = configModel;
-  }
+/**
+ * return whether env belongs to Security settings
+ * @param key ex. 'security:passport-saml:isEnabled' is true
+ * @returns
+ */
+const isSecurityEnv = (key) => {
+  const array = key.split(':');
+  return (array[0] === 'security');
+};
+
+export interface ConfigObject extends Record<string, any> {
+  fromDB: any,
+  fromEnvVars: any,
+}
+
+export default class ConfigLoader {
 
   /**
    * return a config object
    */
-  async load() {
-    const configFromDB = await this.loadFromDB();
-    const configFromEnvVars = this.loadFromEnvVars();
+  async load(): Promise<ConfigObject> {
+    const configFromDB: any = await this.loadFromDB();
+    const configFromEnvVars: any = this.loadFromEnvVars();
 
     // merge defaults per ns
     const mergedConfigFromDB = {
-      crowi: Object.assign(this.configModel.getDefaultCrowiConfigsObject(), configFromDB.crowi),
-      markdown: Object.assign(this.configModel.getDefaultMarkdownConfigsObject(), configFromDB.markdown),
-      notification: Object.assign(this.configModel.getDefaultNotificationConfigsObject(), configFromDB.notification),
+      crowi: Object.assign(defaultCrowiConfigs, configFromDB.crowi),
+      markdown: Object.assign(defaultMarkdownConfigs, configFromDB.markdown),
+      notification: Object.assign(defaultNotificationConfigs, configFromDB.notification),
     };
 
     // In getConfig API, only null is used as a value to indicate that a config is not set.
@@ -485,9 +531,9 @@ class ConfigLoader {
     };
   }
 
-  async loadFromDB() {
+  async loadFromDB(): Promise<any> {
     const config = {};
-    const docs = await this.configModel.find().exec();
+    const docs: Config[] = await ConfigModel.find().exec();
 
     for (const doc of docs) {
       if (!config[doc.ns]) {
@@ -496,12 +542,12 @@ class ConfigLoader {
       config[doc.ns][doc.key] = JSON.parse(doc.value);
     }
 
-    debug('ConfigLoader#loadFromDB', config);
+    logger.debug('ConfigLoader#loadFromDB', config);
 
     return config;
   }
 
-  loadFromEnvVars() {
+  loadFromEnvVars(): any {
     const config = {};
     for (const ENV_VAR_NAME of Object.keys(ENV_VAR_NAME_TO_CONFIG_INFO)) {
       const configInfo = ENV_VAR_NAME_TO_CONFIG_INFO[ENV_VAR_NAME];
@@ -513,11 +559,12 @@ class ConfigLoader {
         config[configInfo.ns][configInfo.key] = configInfo.default;
       }
       else {
-        config[configInfo.ns][configInfo.key] = configInfo.type.parse(process.env[ENV_VAR_NAME]);
+        const parser: ValueParser<number | string | boolean> = parserDictionary[configInfo.type];
+        config[configInfo.ns][configInfo.key] = parser.parse(process.env[ENV_VAR_NAME] as string);
       }
     }
 
-    debug('ConfigLoader#loadFromEnvVars', config);
+    logger.debug('ConfigLoader#loadFromEnvVars', config);
 
     return config;
   }
@@ -527,7 +574,7 @@ class ConfigLoader {
    *
    * **use this only admin home page.**
    */
-  static getEnvVarsForDisplay(avoidSecurity = false) {
+  static getEnvVarsForDisplay(avoidSecurity = false): any {
     const config = {};
     for (const ENV_VAR_NAME of Object.keys(ENV_VAR_NAME_TO_CONFIG_INFO)) {
       const configInfo = ENV_VAR_NAME_TO_CONFIG_INFO[ENV_VAR_NAME];
@@ -537,13 +584,12 @@ class ConfigLoader {
       if (isSecurityEnv(configInfo.key) && avoidSecurity) {
         continue;
       }
-      config[ENV_VAR_NAME] = configInfo.type.parse(process.env[ENV_VAR_NAME]);
+      const parser: ValueParser<number | string | boolean> = parserDictionary[configInfo.type];
+      config[ENV_VAR_NAME] = parser.parse(process.env[ENV_VAR_NAME] as string);
     }
 
-    debug('ConfigLoader#getEnvVarsForDisplay', config);
+    logger.debug('ConfigLoader#getEnvVarsForDisplay', config);
     return config;
   }
 
 }
-
-module.exports = ConfigLoader;

+ 24 - 25
packages/app/src/server/service/config-manager.ts

@@ -1,11 +1,14 @@
-const logger = require('@alias/logger')('growi:service:ConfigManager');
+import parseISO from 'date-fns/parseISO';
 
-const parseISO = require('date-fns/parseISO');
+import loggerFactory from '~/utils/logger';
 
-const S2sMessage = require('../models/vo/s2s-message');
-const S2sMessageHandlable = require('./s2s-messaging/handlable');
+import ConfigModel from '../models/config';
+import S2sMessage from '../models/vo/s2s-message';
+import S2sMessagingService from './s2s-messaging/base';
+import S2sMessageHandlable from './s2s-messaging/handlable';
+import ConfigLoader, { ConfigObject } from './config-loader';
 
-const ConfigLoader = require('./config-loader');
+const logger = loggerFactory('growi:service:ConfigManager');
 
 const KEYS_FOR_LOCAL_STRATEGY_USE_ONLY_ENV_OPTION = [
   'security:passport-local:isEnabled',
@@ -34,24 +37,22 @@ const KEYS_FOR_GCS_USE_ONLY_ENV_OPTION = [
   'gcs:uploadNamespace',
 ];
 
-class ConfigManager extends S2sMessageHandlable {
+export default class ConfigManager implements S2sMessageHandlable {
 
-  constructor(configModel) {
-    super();
+  private configLoader: ConfigLoader = new ConfigLoader();
 
-    this.configModel = configModel;
-    this.configLoader = new ConfigLoader(this.configModel);
-    this.configObject = null;
-    this.configKeys = [];
-    this.lastLoadedAt = null;
+  private s2sMessagingService?: S2sMessagingService;
 
-    this.getConfig = this.getConfig.bind(this);
-  }
+  private configObject: ConfigObject = { fromDB: null, fromEnvVars: null };
+
+  private configKeys: any[] = [];
+
+  private lastLoadedAt?: Date;
 
   /**
    * load configs from the database and the environment variables
    */
-  async loadConfigs() {
+  async loadConfigs(): Promise<void> {
     this.configObject = await this.configLoader.load();
     logger.debug('ConfigManager#loadConfigs', this.configObject);
 
@@ -63,9 +64,9 @@ class ConfigManager extends S2sMessageHandlable {
 
   /**
    * Set S2sMessagingServiceDelegator instance
-   * @param {S2sMessagingServiceDelegator} s2sMessagingService
+   * @param s2sMessagingService
    */
-  async setS2sMessagingService(s2sMessagingService) {
+  setS2sMessagingService(s2sMessagingService: S2sMessagingService): void {
     this.s2sMessagingService = s2sMessagingService;
   }
 
@@ -125,8 +126,8 @@ class ConfigManager extends S2sMessageHandlable {
   getConfigKeys() {
     // type: fromDB, fromEnvVars
     const types = Object.keys(this.configObject);
-    let namespaces = [];
-    let keys = [];
+    let namespaces: string[] = [];
+    let keys: string[] = [];
 
     for (const type of types) {
       if (this.configObject[type] != null) {
@@ -193,7 +194,7 @@ class ConfigManager extends S2sMessageHandlable {
    * ```
    */
   async updateConfigsInTheSameNamespace(namespace, configs, withoutPublishingS2sMessage) {
-    const queries = [];
+    const queries: any[] = [];
     for (const key of Object.keys(configs)) {
       queries.push({
         updateOne: {
@@ -203,7 +204,7 @@ class ConfigManager extends S2sMessageHandlable {
         },
       });
     }
-    await this.configModel.bulkWrite(queries);
+    await ConfigModel.bulkWrite(queries);
 
     await this.loadConfigs();
 
@@ -334,7 +335,7 @@ class ConfigManager extends S2sMessageHandlable {
     const s2sMessage = new S2sMessage('configUpdated', { updatedAt: new Date() });
 
     try {
-      await this.s2sMessagingService.publish(s2sMessage);
+      await this.s2sMessagingService?.publish(s2sMessage);
     }
     catch (e) {
       logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
@@ -362,5 +363,3 @@ class ConfigManager extends S2sMessageHandlable {
   }
 
 }
-
-module.exports = ConfigManager;