laodemalfatih/i18next-node-mongo-backend

View on GitHub
index.js

Summary

Maintainability
A
2 hrs
Test Coverage
// eslint-disable-next-line import/no-extraneous-dependencies
const { MongoClient } = require('mongodb');

const sanitizeFieldNameCharacter = require('./libs/sanitizeFieldNameCharacter');

const defaultOpts = {
  collectionName: 'i18n',
  languageFieldName: 'lang',
  namespaceFieldName: 'ns',
  dataFieldName: 'data',
  sanitizeFieldNameCharacter: true,
  // eslint-disable-next-line no-console
  readOnError: console.error,
  // eslint-disable-next-line no-console
  readMultiOnError: console.error,
  // eslint-disable-next-line no-console
  createOnError: console.error,
  mongodb: {
    useUnifiedTopology: true,
  },
};

// https://www.i18next.com/misc/creating-own-plugins#backend

class Backend {
  /**
   * @param {*} services `i18next.services`
   * @param {object} opts Backend Options
   * @param {string} opts.uri MongoDB Uri
   * @param {string} opts.host MongoDB Host
   * @param {number} opts.port MongoDB Port
   * @param {MongoClient} opts.client Use your custom `MongoClient` instance. Example: `new MongoClient()`
   * @param {string} [opts.username] MongoDB Username
   * @param {string} [opts.user] MongoDB Username
   * @param {string} [opts.password] MongoDB Password
   * @param {string} opts.dbName Database name for storing i18next data
   * @param {string} [opts.collectionName="i18n"] Collection name for storing i18next data
   * @param {string} [opts.languageFieldName="lang"] Field name for language attribute
   * @param {string} [opts.namespaceFieldName="ns"] Field name for namespace attribute
   * @param {string} [opts.dataFieldName="data"] Field name for data attribute
   * @param {boolean} [opts.sanitizeFieldNameCharacter=true] Remove MongoDB special character (contains ".", or starts with "$"). See https://jira.mongodb.org/browse/SERVER-3229
   * @param {function} [opts.readOnError] Error handler for `read` process
   * @param {function} [opts.readMultiOnError] Error handler for `readMulti` process
   * @param {function} [opts.createOnError] Error handler for `create` process
   * @param {object} [opts.mongodb] `MongoClient` Options. See https://mongodb.github.io/node-mongodb-native/3.5/api/MongoClient.html
   * @param {boolean} [opts.mongodb.useUnifiedTopology=true]
   */
  constructor(services, opts = {}) {
    this.init(services, opts);
  }

  // Private methods

  async getClient() {
    const client = this.client || new MongoClient(this.uri, this.opts.mongodb);
    if (!client.isConnected || !client.isConnected()) {
      await client.connect();
    }
    return client;
  }

  async closeClientIfNeeded(client) {
    if (this.client) return;

    await client.close();
  }

  async getCollection(client) {
    const collections = await client
      .db(this.opts.dbName)
      .listCollections()
      .toArray();
    const isCollectionExists = collections.some(
      (collection) => collection.name === this.opts.collectionName,
    );
    if (isCollectionExists) {
      return client.db(this.opts.dbName).collection(this.opts.collectionName);
    }

    return client
      .db(this.opts.dbName)
      .createCollection(this.opts.collectionName);
  }

  sanitizeOpts(opts) {
    this.opts = { ...defaultOpts, ...this.options, ...opts };

    if (this.opts.sanitizeFieldNameCharacter) {
      this.opts.languageFieldName = sanitizeFieldNameCharacter(
        this.opts.languageFieldName,
      );
      this.opts.namespaceFieldName = sanitizeFieldNameCharacter(
        this.opts.namespaceFieldName,
      );
      this.opts.dataFieldName = sanitizeFieldNameCharacter(
        this.opts.dataFieldName,
      );
    }
  }

  // i18next required methods

  init(services, opts, i18nOpts) {
    this.services = services;
    this.i18nOpts = i18nOpts;
    this.sanitizeOpts(opts);

    if (this.opts.client) {
      this.client = this.opts.client;
      return;
    }

    this.uri =
      this.opts.uri ||
      `mongodb://${this.opts.host}:${this.opts.port}/${this.opts.dbName}`;

    const username = this.opts.user || this.opts.username;
    if (username && this.opts.password)
      this.opts.mongodb.auth = {
        user: username,
        username,
        password: this.opts.password,
      };
  }

  async read(lang, ns, cb) {
    if (!cb) return;

    const client = await this.getClient();
    try {
      const col = await this.getCollection(client);
      const doc = await col.findOne(
        {
          [this.opts.languageFieldName]: lang,
          [this.opts.namespaceFieldName]: ns,
        },
        {
          [this.opts.dataFieldName]: true,
        },
      );

      cb(null, (doc && doc[this.opts.dataFieldName]) || {});
    } catch (error) {
      this.opts.readOnError(error);
      cb(error);
    } finally {
      await this.closeClientIfNeeded(client);
    }
  }

  async readMulti(langs, nss, cb) {
    if (!cb) return;

    const client = await this.getClient();
    try {
      const col = await this.getCollection(client);

      const docs = await col
        .find({
          [this.opts.languageFieldName]: { $in: langs },
          [this.opts.namespaceFieldName]: { $in: nss },
        })
        .toArray();

      const parsed = {};

      for (let i = 0; i < docs.length; i += 1) {
        const doc = docs[i];
        const lang = doc[this.opts.languageFieldName];
        const ns = doc[this.opts.namespaceFieldName];
        const data = doc[this.opts.dataFieldName];

        if (!parsed[lang]) {
          parsed[lang] = {};
        }

        parsed[lang][ns] = data;
      }

      cb(null, parsed);
    } catch (error) {
      this.opts.readMultiOnError(error);
      cb(error);
    } finally {
      await this.closeClientIfNeeded(client);
    }
  }

  async create(langs, ns, key, fallbackVal, cb) {
    const client = await this.getClient();
    try {
      const col = await this.getCollection(client);

      // Make `updateOne` process run concurrently
      await Promise.all(
        (typeof langs === 'string' ? [langs] : langs).map((lang) =>
          col.updateOne(
            {
              [this.opts.languageFieldName]: lang,
              [this.opts.namespaceFieldName]: ns,
            },
            {
              $set: {
                [`${this.opts.dataFieldName}.${key}`]: fallbackVal,
              },
            },
            {
              upsert: true,
            },
          ),
        ),
      );

      if (cb && typeof cb === 'function') cb();
    } catch (error) {
      this.opts.createOnError(error);
      if (cb) cb(error);
    } finally {
      await this.closeClientIfNeeded(client);
    }
  }
}

// https://www.i18next.com/misc/creating-own-plugins#make-sure-to-set-the-plugin-type
Backend.type = 'backend';

module.exports = Backend;