/* eslint-disable max-len */
/**
* @typedef {Object} ClientOptions
* @description If you use url you don't need to use username, password, port and host
* @property {string} [username] - Not used if an url is provided
* @property {string} [password] - Not used if an url is provided
* @property {number} [port=27017] - Not used if an url is provided
* @property {string} [host="localhost"] - Not used if an url is provided
* @property {string} [dbName="muffin"] - The name of the database on the Mongo server
* @property {string} [url]
*/
/**
* @typedef {Object} PieceOptions
* @description If you use url you don't need to use username, password, port and host.
* @property {boolean} [fetchAll=false] - Default to false. Caches all the database.
* @since 1.4
* @property {boolean} [autoCacheSync=true] - Default to true. Makes the cache sync itself with the Mongo server.
*/
/**
* @typedef {any} MongoError {@link https://mongodb.github.io/node-mongodb-native/3.3/api/MongoError.html}
*/
const { MongoClient } = require("mongodb"),
EventEmitter = require("events");
const Piece = require("./Piece"),
Err = require("./MuffinError");
const _url = Symbol("url"),
_client = Symbol("client"),
_db = Symbol("db"),
_readyCheck = Symbol("readyCheck"),
_ready = Symbol("ready"),
_readyFailed = Symbol("readyFailed"),
_deprecatedChangeEvent = require("util").deprecate((client, obj) => {
client.emit("change", obj);
}, "MuffinDB : Please consider using Piece's change event instead of Client's change event.");
class MuffinClient extends EventEmitter {
/**
* Use the [MongoDB official Driver]{@link https://www.npmjs.com/package/mongodb} and allows you to create pieces, which are map-like objects. (with an optional cache)
* @constructor
* @public
* @since 1.0
* @extends EventEmitter
* @example
* // Creates a MuffinClient and connects to localhost with port 27017, using "muffin" as db's name
* const client = new Muffin.Client()
* @param {ClientOptions} options - Options for the client.
*/
constructor(options = {}) {
super();
/**
* @since 1.0
* @member {Promise<void>} - Resolved when the database is ready.
*/
this.defer = new Promise((res, rej) => {
try {
if (this.isReady) {
res();
}
this[_ready] = res;
this[_readyFailed] = rej;
} catch (error) {
rej(error);
}
});
/**
* @since 1.0
* @member {string} - Name of the database, Muffin by default.
*/
this.dbName = options.dbName || "muffin";
this[_url] =
options.url ||
`mongodb://${options.username}:${options.password}@${options.host || "localhost"}:${
options.port || 27017
}/${this.dbName}`;
/**
* @since 1.0
* @member {boolean} - True when the database is ready.
*/
this.isReady = false;
/**
* @since 1.0
* @member {boolean} - True if the database is closed.
*/
this.closed = false;
(async () => {
try {
this[_client] = await MongoClient.connect(this[_url], {
useNewUrlParser: true,
useUnifiedTopology: true,
});
this[_db] = this[_client].db(this.dbName);
this.isReady = true;
this[_ready]();
/**
* @event MuffinClient#close
* @since 1.0
* @description Emitted after a socket closed against a single server or mongos proxy.
*/
this[_db].on("close", () => {
this.emit("close");
});
/**
* @event MuffinClient#reconnect
* @since 1.0
* @type {any}
*/
this[_db].on("reconnect", (object) => {
this.emit("reconnect", object);
});
/**
* @event MuffinClient#timeout
* @since 1.0
* @description Emitted after a socket timeout occurred against a single server or mongos proxy.
* @type {MongoError}
*/
this[_db].on("timeout", (err) => {
this.emit("timeout", err);
});
/**
* @event MuffinClient#change
* @deprecated Use [Piece#change]{@link Piece#change} instead
* @since 1.1
* @description Emit when a change occurs on the database.
* @type {any}
*/
this[_db].watch(null, { fullDocument: "updateLookup" }).on("change", (obj) => {
if (this.listenerCount("change") !== 0) {
_deprecatedChangeEvent(this, obj);
}
});
} catch (e) {
this[_readyFailed](e);
}
})();
}
[_readyCheck]() {
if (!this.isReady) {
throw new Err("the database is not ready", "MuffinReadyError");
}
if (this.closed) {
throw new Err("the database has been closed", "MuffinClosedError");
}
}
/**
* @description Creates multiple pieces.
* @since 1.0
* @param {Array<string>} names - Names of the pieces.
* @param {PieceOptions} options - Options like cache or fetchAll.
* @returns {Object<Piece>} An object with the pieces. [Destructuring]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment} can be useful !
* @example
* const Muffin = require("muffindb");
* // MuffinOptions is a placeholder for... muffin options
* const client = new Muffin.Client(MuffinOptions);
*
* // This will returns an object with pieces inside
* // Theses pieces will have a cache because we used options
* // fetchAll will make the pieces fetch the whole data and caches it
* // Their cache will not be synchronized with the database because autoCacheSync is set to false
* const pieces = client.multi(["a_piece", "another_piece", "third_piece"], { fetchAll: true, autoCacheSync: false })
*/
multi(names = [], options) {
this[_readyCheck]();
const pieces = {};
// eslint-disable-next-line array-callback-return
names.map((val) => {
pieces[val] = this.piece(val, options);
});
return pieces;
}
/**
* @description Creates a {@link Piece} to interact with MongoDB.
* @since 1.0
* @param {string} name - The piece's name.
* @param {PieceOptions} options - Options like cache or fetchAll.
* @returns {Piece} A {@link Piece} with the given name
* @example
* const Muffin = require("muffindb");
* // MuffinOptions is a placeholder for muffin options
* const client = new Muffin.Client(MuffinOptions);
*
* // This piece will have a cache because of fetchAll a
* // fetchAll will make the piece fetch the whole data and caches it
* // Its cache will not be synchronized with the database because autoCacheSync is set to false
* const piece = client.piece("a_piece", { fetchAll: true, autoCacheSync: false })
*/
piece(name, options) {
this[_readyCheck]();
return new Piece(this[_db].collection(name), this, options);
}
/**
* @description Close the database.
* @since 1.0
* @returns {void} Nothing
*/
close() {
this[_readyCheck]();
this[_client].close();
this.closed = true;
this.isReady = false;
}
}
module.exports = MuffinClient;