Only this pageAll pages
Powered by GitBook
1 of 24

5.x

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

What is Enmap?

Enmap, the super simple database wrapper with over a million downloads to date. Wrapping around better-sqlite3 with its warm embrace, it's the easiest way to save data in node for your first project!

Enmap stands for "Enhanced Map", and is a data structure based on the native JavaScript Map() structure with additional helper methods from the native Array() structure. Enmap also offers persistence, which means it will automatically save everything to save to it in a database, in the background, without any additional code or delays.

Enmap requires filesystem access. It DOES NOT WORK on Heroku, or other such systems that do not allow you to save data directly to disk.

It should also not be used on Repl.it where the data cannot be hidden (and will be public) or on *Glitch *which has been known to break Enmap's data persistence and lose data.

Why Enmap?

While there are other better-known systems that offer some features of Enmap, especially caching in memory, Enmap is targeted specifically to newer users of JavaScript that might not want to deal with complicated systems or database queries.

Advantage/Disadvantage

Here are some advantages of using Enmap:

  • Simple to Install: Enmap itself only requires a simple npm install command to install and use, and a single line to initialize. See Installation for details.

  • Simple to Use: Basic Enmap usage can be completely done with 1-2 lines of initialization, and 3 commands, set(), get() and delete().

  • Very Fast: Since Enmap resides in memory, accessing its data is blazing fast (as fast as Map() is). Even with persistence, Enmap still accesses data from memory so you get it almost instantly.

Some disadvantages, compared to using a database connection directly:

  • More memory use: Since Enmap resides in memory and (by default) all its data is loaded when it starts, your entire data resides in RAM. When using a large amount of data on a low-end computer or VPS, this might be an issue for some users.

  • Limited power: You can have multiple Enmap "tables" loaded in your app, but they do not and cannot have relationships between them. Basically, one Enmap value can't refer to another value in another Enmap. This is something databases can be very good at, but due to the simplistic nature of Enmap, it's not possible here.

  • Lack of scalability: Enmap is great for small apps that require a simple key/value storage. However, a scalable app spread over multiple processes, shards, or clusters, will be severely limited by Enmap as it cannot update itself from the database on change - one process would not be aware of another process' changes.

Usage Documentation

Mostly, this documentation will be concentrating on the "persistent" version of enmap - the one where data is saved automatically.

If you don't want persistence, the only difference is how you initialize the enmap:

const Enmap = require("enmap");
const myEnmap = new Enmap();

// you can now use your enmap directly

Persistent Enmaps

By default, Enmap saves only in memory and does not save anything to disk. To have persistent storage, you need to add some options. Enmaps with a "name" option will save, and there are additional options you can use to fine-tune the saving and loading features.

const Enmap = require("enmap");

// Normal enmap with default options
const myEnmap = new Enmap({name: "points"});

// non-cached, auto-fetch enmap: 
const otherEnmap = new Enmap({
  name: "settings",
  autoFetch: true,
  fetchAll: false
});

Enmap Options

The following is a list of all options that are available in Enmap, when initializing it:

  • name: A name for the enmap. Defines the table name in SQLite (the name is "cleansed" before use).

    • If an enmap has a name, it is considered persistent and will require better-sqlite-pool to run.

    • If an enmap does not have a name, it is not persistent and any option related to database interaction is ignored (fetchAll, autoFetch, polling and pollingInterval).

  • fetchAll: Defaults to true, which means fetching all keys on load. Setting it to false means that no keys are fetched, so it loads faster and uses less memory.

  • autoFetch: Defaults to true. When enabled, will automatically fetch any key that's requested using get, getProp, etc. This is a "synchronous" operation, which means it doesn't need any of this promise or callback use.

  • dataDir: Defaults to ./data. Determines where the sqlite files will be stored. Can be relative (to your project root) or absolute on the disk. Windows users , remember to escape your backslashes!

  • cloneLevel: Defaults to deep. Determines how objects and arrays are treated when inserting and retrieving from the database.

    • none: Data is inserted by reference, meaning if you change it in the Enmap it changes outside, and vice versa. This should only be used in non-persistent enmaps if you know what you're doing!.

    • shallow: Any object or array will be inserted as a shallow copy, meaning the first level is copied but sub-elements are inserted as references. This emulates Enmap 3's behavior, but is not recommended unless you know what you're doing.

    • deep: Any object or array will be inserted and retrieved as a deep copy, meaning it is a completely different object. Since there is no chance of ever creating side-effects from modifying object, This is the recommended, and default, setting.

  • polling: defaults to false. Determines whether Enmap will attempt to retrieve changes from the database on a regular interval. This means that if another Enmap in another process modifies a value, this change will be reflected in ALL enmaps using the polling feature.

  • pollingInterval: defaults to 1000, polling every second. Delay in milliseconds to poll new data from the database. The shorter the interval, the more CPU is used, so it's best not to lower this. Polling takes about 350-500ms if no data is found, and time will grow with more changes fetched. In my tests, 15 rows took a little more than 1 second, every second.

Troubleshooting Guide

"Anything that can go wrong will go wrong" - Murphy's Law

Please make sure to read the install page VERY CAREFULLY! This should solve all the issues you face. If not carry on, but don't say we didn't tell you.

It's Taking Suuuuuuuuuuuuuuuuuper Long to Install! Whats up with that?

Your computer has to install some programs and then build everything from source. This is going to take a while depending on your computer speed. Patience is key. We can't speed it up for you. It may look like its frozen but it is not.

My Question Isn't Answered Here!

Please make sure you read the install page first. If you can't find what your looking for you can join the discord here.

MSB3428: Could not load the Visual C++ component "VCBuild.exe"

Looks like someone hasn't follows the installation instructions correctly...

Basic Data Use

Now that we have a functional Enmap structure (which we'll always refer to as myEnmap), we're ready to start writing data to it, and getting data from it.

The code samples on this page assume that you have correctly initialized myEnmap

Writing Data

In terms of Enmap, "writing", "adding" and "editing" data is essentially the same thing. When using the basic set() method, if the key does not exist it's created, and if it does, it's modified.

Enmap supports pretty much all native JavaScript data types. However, it cannot support Class Instances directly. That means if you have, say, a "User" object or a "House" class, they cannot be stored here.

There is however a workaround, which is to use .

Objects and Arrays are a little more complex to deal with, so they have their own page. See for more information.

The usage for the set() method is simple:

  • key must be a string or integer. A key should be unique, otherwise it will be overwritten by new values using the same key.

  • value must be a supported native data type as mentioned above.

Here are a few examples of writing simple data values:

Retrieving Data

Getting data back from an Enmap is just as simple as writing to it. All you need is the key of what you want to retrieve, and you get its value back!

That's pretty much it for only retrieving a single data value. There are more complex operations that are available, take a look at for the more advanced things you can do on Enmap's data!

Deleting Data

Removing data from Enmap is as simple as saving or retrieving. You can easily use the delete() method as such:

<Enmap>.set(key, value);
myEnmap.set('boolean', true);
myEnmap.set('integer', 42);
myEnmap.set('someFloat', 73.2345871);
myEnmap.set("Test2", "test2");
const floatValue = myEnmap.get('someFloat');
const test = myEnmap.get('Test2');

// you can even use booleans in conditions: 
if(myEnmap.get('boolean')) {
  // boolean is true!
}
myEnmap.delete("integer");

myEnmap.delete("boolean");
Serializing and Deserializing
Working with Objects
Array Methods

Understanding Paths

What is Paths in Enmap, how to use them, what is their syntax?

In a whole lot of methods for Enmap, one of the properties is the "path". Paths are used in Object data saved in Enmap, that is to say, setting or ensuring a value that is an object at the top level.

To understand what a path really means, we can start by having an object as a value. Here I'm not even using Enmap, as the idea is related to basic JavaScript, not my module.

const myObject = {
  a: "foo",
  b: true,
  c: {
    but: "who",
    are: "you?",
    and: ["are you", "you?"],
  },
  sub: { values: { are: { "cool" } } },
};

So here we have an object that actually has multiple levels, that is to say, the c and sub properties have, as a value, another object with its own keys. sub takes this further with 4 different levels, just to fully demonstrate my point.

So how would we reach the values in this object? Well, in core JavaScript, let's say we wanted to get the word "cool", we'd use myObject.sub.values.are.cool. This is one way to access object properties, the other one being myObject["sub"]["values"]["are"]["cool"] (where those strings can be variables, btw, for dynamic property access).

Alright so what about the array, there? Well, arrays are accessed through their index, meaning their position in the array, starting at 0. That means to access the c.and values, you'd do something like myObject.c.and[0] . That looks like a strange syntax I'll admit, but considering you can use the same for objects, myObject["c"]["and"][1] perhaps looks a bit more coherent.

Doing it in Enmap

Now that you've seen how to access those properties in regular JavaScript, what about doing it in Enmap? Well, it's actually quite simple: the path parameter in the methods simply take exactly what you've seen above, with 2 exceptions:

  • The path doesn't include the object name (which is your key)

  • You don't need to use variables for dynamic paths since it's a string

What does that mean in reality? Well let's rewrite the example above as Enmap code:

myEnmap.set("myObject", {
  a: "foo",
  b: true,
  c: {
    but: "who",
    are: "you?",
    and: ["are you", "you?"],
  },
  sub: { values: { are: { "cool" } } },
});

To access the "cool" string, the code then becomes myEnmap.get("myObject", "sub.values.are") . Accessing the array values looks the same: myEnmap.get("myObject", "c.and[0]") . In this case indexes can be used either way, so you can also do myEnmap.get("myObject", "c.and.0") and that'll work equally well.

Enmap Installation

To install Enmap, please read these instructions very carefully, every word is important!

Enmap is a wrapper around better-sqlite3, which requires to be built directly on your system. As such, you need to install pre-requisites first. Please follow these instructions to the letter. If it's not written here, you probably shouldn't do it unless you know why you're doing it.

Pre-Requisites

SQLite modules usually only successfully work on LTS versions of node. This means it will work correctly on node 12, and 14. It will not work on node 13, 15, 17. Make sure you have the right version, check this with node -v.

Also, better-sqlite3 doesn't compile on unreleased node versions usually. Your mileage may vary, but don't expect it to work on the "Latest" version of node either.

How to install the pre-requisites depends on your operating system, so see below for instructions:

On Windows, two things are required to install better-sqlite3. Python, and the Visual Studio C++ Build Tools. They are required for any module that is built on the system, which includes sqlite.

To install the necessary prerequisites on Windows, the easiest is to simply run the following commands separately, under an administrative command prompt or powershell:

// First run:
npm i -g --add-python-to-path --vs2015 --production windows-build-tools

// Then run:
npm i -g node-gyp@latest

It's very important that this be run in the administrative prompt, and not a regular one.

Once the windows-build-tools are installed (this might take quite some time, depending on your internet connection), close all open command prompts, powershell windows, and editors with a built-in console/prompt. Otherwise, the next command will not work.

On Linux, the pre-requisites are much simpler in a way. A lot of modern systems (such as Ubuntu, since 16.04) already come with python 2.7 pre-installed. For some other systems, you might have to fiddle with it to either get python 2.7 installed, or to install both 2.7 and 3.x simultaneously. Google will be your friend.

As for the C++ build tools, that's installed using the simple command: sudo apt-get install build-essential for most debian-based systems. For others, look towards your package manager and specificall "GCC build tools". Your mileage may vary but hey, you're using Linux, you should know this stuff.

As of writing this page, MacOS versions seem to all come pre-built with Python 2.7 on the system. You will, however, need the C++ build tools.

  • Install XCode

  • Once XCode is installed, go to Preferences, Downloads, and install the Command Line Tools.

Once installed, you're ready to continue.

Installing Enmap

Once those pre-requisites are installed (if they're not, scroll up, and follow the instructions), and you've closed all open command prompts, open a new, normal (not-admin) command prompt or terminal in your project, then install Enmap using the following command:

npm i enmap

This will take a few minutes, as it needs to build better-sqlite3 from source code, and then install enmap itself. Note that "a few minutes" can be 1 or 30 minutes, it really depends on your hardware and configuration.

If you get any errors, please see the Troubleshooting Guide. If the guide doesn't help, join the Discord (link at the top of this page).

Working with Objects

Enmap is a great way to store structured data, and offers a few helper features that directly affect both objects and arrays.

Let's assume for a moment that we want to store the following data structure in Enmap:

const myStructure = {
  first: "blah",
  second: "foo",
  changeme: "initial",
  isCool: false
  sub: {
    yay: true,
    thing: "amagig"
  }
}

This structure has 5 "properties": first, second, changeme, isCool, sub. The sub property has 2 properties of its own, yay and thing.

To store this structure in Enmap, you can use a variable, or just straight-up write the object:

myEnmap.set("someObject", myStructure);

// Or directly the object
myEnmap.set("someObject", {first: "blah", ...});

// Works with arrays, too!
myEnmap.set("someArray", ["one", "two", "three"]);

Note: All further methods require the value to be an object. If you attempt to get, set, modify or remove using the below methods and your value isn't an object, Enmap will throw an error.

Getting properties

Retrieving a specific property from an object is done through the get() method, by specifying both the key and the "path" to the property you want.

The exact method is <Enmap>.get(key, path).

const second = myEnmap.get("someObject", "second");
// returns "foo"

const thing = myEnmap.get("someObject", "sub.yay");
// returns true

// The path can be dynamic, too: 
const propToGet = "thing";
const blah = myEnmap.get("someObject", `sub.${propToGet}`);

Checking if a property exists

You can also check if a specific property exists or not. This is done through the has method, with a key, and path to the property:

myEnmap.has("someObject", "sub.thing"); // returns true

myEnmap.has("someObject", "heck"); // returns false.

Modifying Properties

There are a few various ways to modify properties of both Objects and Arrays. The very basic way to set a property on an object or array is through .set(key, value, path) like the following examples:

// Set an object property
myEnmap.set("someObject", "newThing", "sub.blah");

// Set an array property
myEnmap.set("someArray", "four", 3);

As you can see, setProp() and getProp() work on the same concept that the path can be as complex as you want.

Arrays have additional helper methods, you can see them here.

Using the fetchAll option

As described in the home page, one disadvantage of Enmap is that it loads all your data in memory, so you're sacrificing RAM in order to gain speed. In larger projects, this might become a concern fairly quickly - or when using larger data sets that take more memory.

For this purpose, there are features in Enmap that enable less caching, by sacrificing some speed and ease of use. That is to say, with data not being fully loaded, there are some things that can't be done easily - see below for details.

The options are as follow:

  • fetchAll: Defaults to true, which means fetching all keys on load. Setting it to false means that no keys are fetched, so it loads faster and uses less memory.

    autoFetch: Defaults to true. When enabled, will automatically fetch any key that's requested using get, getProp, etc. This is a "synchronous" operation, which means it doesn't need any of this promise or callback use.

const Enmap = require("enmap");

const points = new Enmap({
  name: "points",
  fetchAll: false,
  autoFetch: true
});

What does it mean if the data isn't loaded?

If fetchAll is set to false, no data (by default) will be loaded from the database - the Enmap will be completely empty. This means that doing a .size check returns 0, looping and filtering doesn't return anything, and get() requests all return null.

Ok but... how's that useful? It's useful because if you don't need the data, it's not loaded. To load data, there are 2 different methods available.

Fetching Data

  • enmap.fetchEverything() will, of course, fetch all the data from the database - this is the method that's called on init() if fetchAll is true. This means you can stagger your loading time, or that you can decide, later, under certain conditions, you do want to load everything.

  • enmap.fetch(key) can fetch a single key from the database, saves it to enmap, and returns a promise with the fetched value.

  • enmap.fetch([array, of, keys]) will fetch each key in the requested array, and return an array of [key, value] pairs for each fetched value.

  • enmap.count will give you the number of keys in the database itself, including uncached ones (.size is only cached values).

  • enmap.indexes will give you a list of keys in the database, including uncached ones.

That's it!

Yup. Those are the only things you really need to know for the current version of Enmap's fetchAll feature.

Using from multiple files

This page will describe how to use Enmap from multiple files within your same project. Note that I mean the same app, process, or shard, but different files within this one running process.

A common issue

When Enmap is used with its default options, it loads everything in its cache and generally provides your data from this cache, not directly from the database. In the case where you want to use the data from one Enmap from multiple locations, you might encounter the following issue:

Hi! When I update data in Enmap from one file, it doesn't update in the other file, I have to restart the bot to update. Is this a bug?

To answer my own obvious question: it's not a bug, it's a feature that I cannot implement. The way Enmap's cache works is that the data is loaded in memory _in that __instance _of Enmap, and only for that instance. This is what enables you to have many different Enmaps in your project - one Enmap doesn't share data with another.

However, this also means that when you do new Enmap({ name: "something" }) from more than one file, that's also a different instance, that doesn't share the same memory space. So not only will it not update the data in memory for the other file, it also uses double the memory. And of course, that's bad. So how do we fix this?

The Shared Variable Method

Admittedly, the vast majority of you Enmap users are doing Discord.js Bots, and even though Enmap works fine with any nodejs project that need simple data storage, bots are my main clients. Considering this fact, we have an extremely simple way to share an Enmap between multiple files: We attach it to the bot client. Usually your client is defined in your main file (index.js, app.js, bot.js, whatever you named it), and every part of your bot has access to this client. We can attach Enmap directly to it, like so:

const Discord = require("discord.js");
const client = new Discord.Client();

const Enmap = require("enmap");

// this is the important bit
client.settings = new Enmap({ name: "settings" });
client.tags = new Enmap({ name: "tags" });

// your normal events here
client.on("message", message => {
  const guildSettings = client.settings.get(message.guild.id);
  // works here
});

client.login(token);

This will work even if you're using a command handler, framework, or whatever - as long as you have access to a client variable, you have access to your enmaps.

Important Note: Do NOT override Discord.js' existing collections! That means, client.users, client.guilds, etc. See all the properties and methods for the Discord.js client - none of these should be overridden.

In other frameworks and libraries, you might have something similar. For example with Express or Koa for http servers, you can sometimes attach the enmap to your request from the very top, in a middleware. If that's not possible, or if you find that to be complicated, you can use the next method.

The Module Method

All things considered, modules are probably the recommended way to use your Enmap in multiple files within your project. Not only does it give you a single file to import, lets you define multiple Enmaps you can individually import, it also gives you the ability to add specific functions to do common actions you use throughout your project.

As covered in My JavaScript Guide, modules are fairly straightforward. This is how I have done an Enmap shared module before:

const Enmap = require("enmap");

module.exports = {
  settings: new Enmap({
    name: "settings",
    autoFetch: true,
    fetchAll: false
  }),
  users: new Enmap("users"),
  tags: new Emmap({ name : "tags" })
}

This means you can simply require that file elsewhere. Let's say we called that file db.js , here's how you'd use it:

const db = require("./db.js");

console.log(db.settings.size);
db.tags.set("blah", {
  guild: "1234",
  author: "4231",
  name: "blah",
  content: "I'm bored, mommy!"
});

And as I mentioned, as a bonus you now have the ability to create functions which you can export and use, to simplify your code and remove duplication. So, let's say I need to get all the tags for a specific guild, and my tags are built using an object as shown above. To get all those tags for a guild, you'd need filters, right? Like so:

const guildTags = db.tags.find(tag => tag.guild === message.guild.id);

now let's say you use this code a lot in your app, and you'd like to not have to type this whole thing every time. You could add a simple function in your module that only takes an ID and returns the tags:

const Enmap = require("enmap");

module.exports = {
  settings: new Enmap({
    name: "settings",
    autoFetch: true,
    fetchAll: false
  }),
  users: new Enmap("users"),
  tags: new Emmap({ name : "tags" }),
  getTags: (guild) => {
    return this.tags.find(tag => tag.guild === message.guild.id);
  }
}

And there you have it! There are other ways to build the exports, you can also split it differently, take a look at My Modules Guide for more information.

Array Methods

While Enmap is a Map enhanced with Array methods, Enmap also offers some enhanced array methods for the data stored inside of it. Talk about Arrayception!

So what do I mean by methods for your stored data? I mean that you can store arrays inside Enmap, and directly push, pull, add and remove from those arrays. There are methods to work both on direct arrays, as well as arrays stored inside of an object.

Let's take a look at three example entries in Enmap that we can use. The first is a direct array, the second is an array inside an object, the last is an array of objects.

myEnmap.set("simpleArray", [1,2,3,4,5]);

myEnmap.set("arrInObj", {
  name: "Bob",
  aliases: ["Bobby", "Robert"]
});


myEnmap.set('objectarray', [{ a: 1, b: 2, c: 3 }, { d: 4, e: 5, f: 6 }]);

Adding to the array

There are two methods to push to an array, one for simple arrays and one for arrays inside objects. Pushing in an Enmap array is the same as a regular array push: it adds the element to the end of the array.

myEnmap.push("simpleArray", 6);
// now [1,2,3,4,5,6]

myEnmap.push("arrInObj", "Robby", "aliases");
// now ["Bobby", "Robert", "Robby"]

The third parameter in push is the "path" to the array in an object. It works the same as the properties path used in Working With Objects.

Removing from the array

Similarly, you can remove from an array. With the normal path system, you can either remove via the index in the array, or remove simple strings. To remove a complex object, you'll need to use a function in the remove method.

myEnmap.remove("simpleArray", 2);
// now [1,3,4,5,6]

myEnmap.remove("arrInObject", "Bobby", "aliases");
// now ["Robert", "Robby"]

myEnmap.remove('objectarray', (value) => value.e === 5);
// value is now [{ a: 1, b: 2, c: 3 }]

Examples

D.js Points/Currency

This points bot is simple, but functional. Make sure you've followed the Installation Instructions before doing the following.

First, you need to create a new persistent Enmap. Here's how it goes:

const Enmap = require("enmap");
client.points = new Enmap("points");

That will create a new Enmap under the name of points, and attaches it to the client object so it can be used where ever you have access to the client object.

Accumulating Points

The obvious goal of a points system is to accumulate fake internet points and gloat about it. So, of course, that's going to be our first focus. In this example implementation, we will make the points guild-specific, and unusable in DMs. Points will still accumulate even if the user does a command, which simplifies our code a bit.

Our starting point is a very basic message handler with pre-existing commands - such as what we see in the Command with Arguments page on An Idiot's Guide. The code is as such:

client.on("message", message => {
  if (message.author.bot) return;
  // This is where we'll put our code.
  if (message.content.indexOf(config.prefix) !== 0) return;

  const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
  const command = args.shift().toLowerCase();

  // Command-specific code here!
});

We do have a small caveat - we really don't want to react on Direct Messages, so our whole code will be in a block that checks for that:

client.on("message", message => {
  if (message.author.bot) return;
  if (message.guild) {
    // This is where we'll put our code.
  }
  // Rest of message handler
});

Our very first step is going to be to initialize a new entry in the enmap for any new user - one we haven't received a message from before. This is done using the enmap.ensure(key, defaultvalue) method, which can check if a specific key exists in the Enmap, write it if it doesn't (and return the defaultvalue in this case). Note that our keys take the format of guildid-userid so they're unique to the guild and the user. Also, our data in this case is a complete object, which we'll take advantage of fully.

client.on("message", message => {
  if (message.author.bot) return;
  if (message.guild) {
    client.points.ensure(`${message.guild.id}-${message.author.id}`, {
      user: message.author.id,
      guild: message.guild.id,
      points: 0,
      level: 1
    });
    // Code continued...
  }
  // Rest of message handler
});

There's obviously a few ways we could have done this, including some fancy ternary condition or whatever. I will, however, keep this code as simple to read as possible.

The following bit is super simple - Enmap has a method to directly increment a value in Enmap even if it's in an object. Pretty clever if I do say so myself!

client.on("message", message => {
  if (message.author.bot) return;
  if (message.guild) {
    // Let's simplify the `key` part of this.
    const key = `${message.guild.id}-${message.author.id}`;
    client.points.ensure(key, {
      user: message.author.id,
      guild: message.guild.id,
      points: 0,
      level: 1
    });
    client.points.inc(key, "points");
  }
  // Rest of message handler
});

Have your own way of incrementing points? No problem! Enmap.math() gives you the ability to add, multiply, and act upon any numerical value or property. To add 10 points, for instance, client.points.math(key, "+", 10, "points") would be used.

Ding!

Time to level up! If a user has enough points, they will go up a level. Now we have to do some math here, but don't run off in fear, this one's pretty easy. This is how we calculate the levels:

const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));

This line will calculate the square root of currentPoints then multiplies that result by 0.1 then floors that result for a round number.

Now we should work out if you've amassed enough points to actually level up, by grabbing the current user's level and comparing them. If the new calculated level is higher, it means the user leveled up and we can deal with that, first by sending them a very annoying mee6-inspired message!

if (client.points.get(key, "level") < curLevel) {
  message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
}

Lastly, we want to update the score.level value with the new level so throw this under the message.reply.

client.points.set(key, curLevel, "level");

So here's the whole thing from top to bottom, with bonus comments!

client.on("message", message => {
  // As usual, ignore all bots.
  if (message.author.bot) return;

  // If this is not in a DM, execute the points code.
  if (message.guild) {
    // We'll use the key often enough that simplifying it is worth the trouble.
    const key = `${message.guild.id}-${message.author.id}`;

    // Triggers on new users we haven't seen before.
    client.points.ensure(`${message.guild.id}-${message.author.id}`, {
      user: message.author.id,
      guild: message.guild.id,
      points: 0,
      level: 1
    });

    client.points.inc(key, "points");

    // Calculate the user's current level
    const curLevel = Math.floor(0.1 * Math.sqrt(client.points.get(key, "points")));

    // Act upon level up by sending a message and updating the user's level in enmap.
    if (client.points.get(key, "level") < curLevel) {
      message.reply(`You've leveled up to level **${curLevel}**! Ain't that dandy?`);
      client.points.set(key, curLevel, "level");
    }
  }
  // Rest of message handler
});

Points & Level Commands

Alright, that's the bulk of the code, you could throw this into your bot and it would work like a charm, however your users wouldn't know how many points, or even their levels, so let's fix that, make a new command called points, which will also show them their level.

Obviously there's no way for us to know how you're making commands, so again we'll assume you're doing a bot in a single js file. You may need to adjust the code, of course!

So let's re-iterate our current starting position.

client.on("message", message => {
  if (message.author.bot) return;
  if (message.guild) { /* Points Code Here */ }
  if (message.content.indexOf(config.prefix) !== 0) return;

  const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
  const command = args.shift().toLowerCase();

  // Command-specific code here!
});

The points command would look like this:

  if (command === "points") {
    const key = `${message.guild.id}-${message.author.id}`;
    return message.channel.send(`You currently have ${client.points.get(key, "points")} points, and are level ${client.points.get(key, "level")}!`);
  }

We are the champions, my friend!

Let's finish this off with a very simple leaderboard command that will show the top 10 users in the current guild. For this we'll need to filter the Enmap to only get the users for the current guild, then we'll convert the results to an array, sort that, and keep the first 10 results only.

We convert to an array because an Enmap, just like its underlying Map structure, is not ordered and thus cannot be sorted. It may seem ordered because it stores by keys, but that's actually a quirk, not a feature.

So here's our leaderboard command:

if(command === "leaderboard") {
  // Get a filtered list (for this guild only), and convert to an array while we're at it.
  const filtered = client.points.filter( p => p.guild === message.guild.id ).array();

  // Sort it to get the top results... well... at the top. Y'know.
  const sorted = filtered.sort((a, b) => b.points - a.points);

  // Slice it, dice it, get the top 10 of it!
  const top10 = sorted.splice(0, 10);

  // Now shake it and show it! (as a nice embed, too!)
  const embed = new Discord.MessageEmbed()
    .setTitle("Leaderboard")
    .setAuthor(client.user.username, message.guild.iconURL())
    .setDescription("Our top 10 points leaders!")
    .setColor(0x00AE86);
  for(const data of top10) {
    try {
      embed.addField(client.users.cache.get(data.user).tag, `${data.points} points (level ${data.level})`);
    } catch {
      embed.addField(`<@${data.user}>`, `${data.points} points (level ${data.level})`);
    }
  }
  return message.channel.send({embed});
}

ADDENDUM: Extra Commands!

  if(command === "give") {
    // Limited to guild owner - adjust to your own preference!
    if(message.author.id !== message.guild.ownerID) 
      return message.reply("You're not the boss of me, you can't do that!");

    const user = message.mentions.users.first() || client.users.get(args[0]);
    if(!user) return message.reply("You must mention someone or give their ID!");

    const pointsToAdd = parseInt(args[1], 10);
    if(!pointsToAdd) 
      return message.reply("You didn't tell me how many points to give...")

    // Ensure there is a points entry for this user.
    client.points.ensure(`${message.guild.id}-${user.id}`, {
      user: message.author.id,
      guild: message.guild.id,
      points: 0,
      level: 1
    });

    // Get their current points.
    let userPoints = client.points.get(`${message.guild.id}-${user.id}`, "points");
    userPoints += pointsToAdd;


    // And we save it!
    client.points.set(`${message.guild.id}-${user.id}`, userPoints, "points")

    message.channel.send(`${user.tag} has received **${pointsToAdd}** points and now stands at **${userPoints}** points.`);
  }

  if(command === "cleanup") {
    // Let's clean up the database of all "old" users, 
    // and those who haven't been around for... say a month.

    // Get a filtered list (for this guild only).
    const filtered = client.points.filter( p => p.guild === message.guild.id );

    // We then filter it again (ok we could just do this one, but for clarity's sake...)
    // So we get only users that haven't been online for a month, or are no longer in the guild.
    const rightNow = new Date();
    const toRemove = filtered.filter(data => {
      return !message.guild.members.cache.has(data.user) || rightNow - 2592000000 > data.lastSeen;
    });

    toRemove.forEach(data => {
      client.points.delete(`${message.guild.id}-${data.user}`);
    });

    message.channel.send(`I've cleaned up ${toRemove.size} old farts.`);
  }

Migrating data from Enmap 3

This guide assists in migrating your data from Enmap 3 using Providers, to the latest version of enmap.

You do not need this page if you're new to Enmap or if you're starting a new project!

Upgrading to enmap v4 requires a little bit of migration, as Enmap 4 changed the internal method by which data is stored, slightly. To use this migration:

  • Make a copy of your current app in a new folder.

  • Create a new folder "on the same level" as your bot. Name it something like "migrate"

  • You should now have 3 folders. Something like mybots/coolbot , mybots/coolbot-copy , mybots/migrate/

  • In the migrate folder, run npm i enmap@3.1.4 enmap-sqlite@latest , as well as whatever source provider you need if it's not sqlite (in my example, npm i enmap-mongo@latest

You should now have something like the following image.

In the migrate folder, create an index.js and use the following script for migration. Note that it's an example, change the provider option to fit what you're actually using.

const Enmap = require("enmap");
const Provider = require("enmap-mongo");
const SQLite = require("enmap-sqlite");

let options = { 
  name: "test",
  user: "username",
  host: "yourhost",
  collection: "enmap",
  password: "password",
  port: 55258
};

const source = new Provider(options); 
const target = new SQLite({"name": "test", dataDir: '../coolbot-copy/data'});
Enmap.migrate(source, target).then( () => process.exit(0) );

Very important: the "target" must be enmap-sqlite. Enmap v4 only supports an sqlite-backend.

From the migrate folder, run node index.js, which should correctly migrate your data.

Simpler migration from enmap-sqlite

If you're using enmap-sqlite already, you don't really need to do the entire thing above. Adding a single file called migrate.js to your project folder, then running it with node migrate.js will convert the format and then all you need is to modify the code for Enmap 4. Still, I recommend backing up your bot first. Just in case.

const Enmap = require("enmap");
const SQLite = require("enmap-sqlite");

const source = new SQLite({"name": "test"});
const target = new SQLite({"name": "test"});
Enmap.migrate(source, target).then( () => process.exit(0) );

Code Changes

There is very little you need to change when moving to Enmap 4. The only changes that are required after migrating is the initialization of your Enmap which is now simpler.

// Change From: 
const Enmap = require("enmap");
const Provider = require("enmap-mongo");

client.points = new Enmap({provider: new Provider({name: "points", url: "blah"});

// Change To: 
const Enmap = require("enmap");
client.points = new Enmap({name: "points"});

If using Enmap.multi(), the change is just as simple:

// Change from V3:  
const Enmap = require("enmap");
const Provider = require("enmap-mongo");

Object.assign(client, Enmap.multi(["settings", "tags"], Provider, { url: "blah" }));

// Change to V4: 
const Enmap = require("enmap");
Object.assign(client, Enmap.multi(["settings", "tags"]));

The rest of your code (all interactions with Enmap) can remain the same - there should be no need to edit any of it.

Installing V4

Once your data is migrating and the code is changed, you can go ahead and install enmap version 4 through npm i enmap@latest in your "new" bot folder (the target of the migration). This will take a few minutes (it needs to rebuild sqlite) and output that 4.0.x is now installed. Start the bot, and it should be working! If it doesn't, join the support server and we'll help you out ^_^.

D.js Per-Server Settings

This example uses a very, very simple bot made in discord.js to demonstrate how easily can be used to create a per-server configuration system.

Remember to follow the before running any of this!

Initializing

Our first task is of course to initialize the enmap correctly. In this case, we're attaching the settings to our client object so we can use it in different commands.

Events Setup

Message Event

The main event for our bot, where messages are received. Any error here will probably crash the bot on every message received, so be careful!

Command to set configurations

Command to show the configuration

// start discord.js init
// config with token and prefix.
const config = require("./config.json"); 
// Code below supports and is tested under "stable" 11.3.x
const Discord = require("discord.js");
const client = new Discord.Client();
// end discord.js init

// Initialize the server configurations
const Enmap = require('enmap');

// I attach settings to client to allow for modular bot setups
// In this example we'll leverage fetchAll:false and autoFetch:true for
// best efficiency in memory usage. We also have to use cloneLevel:'deep'
// to avoid our values to be "reference" to the default settings.
// The explanation for why is complex - just go with it.
client.settings = new Enmap({
  name: "settings",
  fetchAll: false,
  autoFetch: true,
  cloneLevel: 'deep',
  autoEnsure: {
    prefix: "!",
    modLogChannel: "mod-log",
    modRole: "Moderator",
    adminRole: "Administrator",
    welcomeChannel: "welcome",
    welcomeMessage: "Say hello to {{user}}, everyone!"
  }
});
client.on("guildDelete", guild => {
  // When the bot leaves or is kicked, delete settings to prevent stale entries.
  client.settings.delete(guild.id);
});

client.on("guildMemberAdd", member => {
  // This executes when a member joins, so let's welcome them!

  // First, ensure the settings exist
  client.settings.ensure(member.guild.id, defaultSettings);

  // First, get the welcome message using get: 
  let welcomeMessage = client.settings.get(member.guild.id, "welcomeMessage");

  // Our welcome message has a bit of a placeholder, let's fix that:
  welcomeMessage = welcomeMessage.replace("{{user}}", member.user.tag)

  // we'll send to the welcome channel.
  member.guild.channels.cache
    .find(channel => channel.name === client.settings.get(member.guild.id, "welcomeChannel"))
    .send(welcomeMessage)
    .catch(console.error);
});
client.on("message", async (message) => {
  // This stops if it's not a guild (obviously), and we ignore all bots.
  // Pretty standard for any bot.
  if(!message.guild || message.author.bot) return;

  // We get the value, and autoEnsure guarantees we have a value already.
  const guildConf = client.settings.get(message.guild.id);

  // Now we can use the values! We stop processing if the message does not
  // start with our prefix for this guild.
  if(message.content.indexOf(guildConf.prefix) !== 0) return;

  //Then we use the config prefix to get our arguments and command:
  const args = message.content.split(/\s+/g);
  const command = args.shift().slice(guildConf.prefix.length).toLowerCase();

  // Commands Go Here
});
  // Alright. Let's make a command! This one changes the value of any key
  // in the configuration.
  if(command === "setconf") {
    // Command is admin only, let's grab the admin value: 
    const adminRole = message.guild.roles.cache.find(role => role.name === guildConf.adminRole);
    if(!adminRole) return message.reply("Administrator Role Not Found");

    // Then we'll exit if the user is not admin
    if(!message.member.roles.cache.has(adminRole.id)) {
      return message.reply("You're not an admin, sorry!");
    }

    // Let's get our key and value from the arguments. 
    // This is array destructuring, by the way. 
    const [prop, ...value] = args;
    // Example: 
    // prop: "prefix"
    // value: ["+"]
    // (yes it's an array, we join it further down!)

    // We can check that the key exists to avoid having multiple useless, 
    // unused keys in the config:
    if(!client.settings.has(message.guild.id, prop)) {
      return message.reply("This key is not in the configuration.");
    }

    // Now we can finally change the value. Here we only have strings for values 
    // so we won't bother trying to make sure it's the right type and such. 
    client.settings.set(message.guild.id, value.join(" "), prop);

    // We can confirm everything's done to the client.
    message.channel.send(`Guild configuration item ${prop} has been changed to:\n\`${value.join(" ")}\``);
  }
  if(command === "showconf") {
    let configProps = Object.keys(guildConf).map(prop => {
      return `${prop}  :  ${guildConf[prop]}`;
    });
    message.channel.send(`The following are the server's current configuration:
    \`\`\`${configProps.join("\n")}\`\`\``);
  }
Enmap
Installation Instructions

Blog Posts

Serializing and Deserializing

Learn how to manipulate the data you save and retrieve from the database, to more easily store complex data without having to convert it to simple data everywhere you use it.

Introduced in Enmap 5.6, Serializers and Deserializers are functions that you use to manipulate the data before storing it in the database, or before using it after retrieving it.

This feature is born from a limitation in Enmap: it cannot store very complex objects, such as the instance of a class, objects with circular references, functions, etc. So, typically when you have such data, you need to manually convert it to some simple representation before storing, and then do the inverse after getting it from enmap. This is a more automated way of doing it.

What are they?

The Serializer function runs every single time data is stored in the enmap, if one is provided. This function receives the data provided to set() as an input, and must return a value to be stored in the database. This function MUST be synchronous, that is to say, cannot be an async function or return a promise.

// the default serializer
const serializer = (data, key) => {
  return data;
};

The Deserializer function is the reverse, and runs on each value pulled from the database, before it is returned through the get() method. This function receives the data stored in the database and returns the value that you want to use directly. This function MUST be synchronous, that is to say, cannot be an async function or return a promise.

// the default deserializer
const deserializer = (data, key) => {
  return data;
};

Examples

Guild Settings: A more sensible example

Taking a hit from my own example of Per-Server Settings, this is a better example that doesn't require storing just the name of a channel, but straight-up the channel itself.

// Imagine the client and stuff is already defined.


// The function that runs when storing data
const serializeData: data => {
 return {
   ...data,
   // stores the guild as ID
   guild: guild.id,
   // stores the user as ID
   user: user.id,
 }
};

// This one runs when loading.
const deserializeData: data => {
  return {
    ...data,
    // gets the guild itself from the cache from its ID
    guild: client.guilds.cache.get(data.guild),
    // Same with the user!
    user: client.users.cache.get(data.user),
  }
};

// Default Settings can no longer store defaults for roles and channels.
const defaultSettings = {
  prefix: "!",
  modLogChannel: null,
  modRole: null,
  adminRole: null,
  welcomeChannel: null,
  welcomeMessage: "Say hello to {{user}}, everyone!"
}

// Our enmap has shiny new options here!
client.settings = new Enmap({
  name: "settings",
  cloneLevel: 'deep',
  serializer: serializeData,
  deserializer: deserializeData,
  // Might as well autoensure, eh?
  autoEnsure: defaultSettings,
});


// Store some data, obviously needs to be run in the right place: 
client.settings.set(message.guild.id,
  message.mentions.channels.first(),
  'welcomeChannel'
);

client.settings.set(message.guild.id,
  message.mentions.roles.first(),
  'adminRole'
);

// GET the data after
const welcomeChannel = client.settings.get(message.guild.id, 'welcomeChannel');
welcomeChannel.send("This works without having to find or get the channel!");

Mathematical Methods

This page is a work in progress and may not have the polish of a usual Evie-Written document!

Some quick docs:

enmap.math(key, operation, operator, [objectPath])

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});
 
points.math("number", "/", 2); // 21
points.math("number", "add", 5); // 26
points.math("number", "modulo", 3); // 2
points.math("numberInObject", "+", 10, "sub.anInt");

Possible Operators (accepts all variations listed below, as strings):

  • +, add, addition: Increments the value in the enmap by the provided value.

  • -, sub, subtract: Decrements the value in the enmap by the provided value.

  • *, mult, multiply: Multiply the value in the enmap by the provided value.

  • /, div, divide: Divide the value in the enmap by the provided value.

  • %, mod, modulo: Gets the modulo of the value in the enmap by the provided value.

  • ^, exp, exponential: Raises the value in the enmap by the power of the provided value.

enmap.inc(key, [objectPath])

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});
 
points.inc("number"); // 43
points.inc("numberInObject", "sub.anInt"); // {sub: { anInt: 6 }}

enmap.dec(key. [objectPath])

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});
 
points.dec("number"); // 41
points.dec("numberInObject", "sub.anInt"); // {sub: { anInt: 4 }}

Koa Authentication

In this example we'll be using Enmap to store user data in order to authenticate users on a simple Koa application. In order to make this secure, we'll be using bcrypt to encrypt the passwords, so of course they will not be plain text in the database.

Requirements

We'll be using koa as a web server module, along with koa-session in order to store the session information. Additionally, for ease of use, koa-router is used to simplify the route code.

This tutorial uses koa-session which, by default, is insecure since it stores the entire session data in a browser cookie. This means the password, though encrypted, would be availble in the cookie, and easy to spoof. There are many session stores available for different storage system, but using them is beyond the scope of this example.

To install those requirements, run the following in a new, empty folder for your project:

Once all of those are installed, we're ready to start! We're going to create an index page, login page, and one "protected" page that requires login. Let's start with the top of the file, which is all the required modules:

So now we have all the basics necessary.

Account Creation Function

Let's create a few functions specifically for the login features, related to enmap. First, a function to create a new user:

This function takes in the following arguments:

  • username which obviously is self-explanatory: it's the username entered during login.

  • name is the "full name" or "display name", for a friendly display on the page or an email.

  • plainpw is the password desired for this account. It has to come in as plain text, obviously, in order to be properly saved in the database.

  • admin is a boolean value representing whether the user should be administrator. If creating something like a blog, only an administrator could create other administrators. It's false by default.

The function first checks if the username exists and returns an error if it does. It then generates a salted, hashed version of the password which it stores in the database. Don't let the name fool you, the password is not "encrypted", which implies that it can be decrypted. Instead, it's a "cryptographic hash functions", a unidirectional function that cannot be undone. The only way to verify that a password is correct is to re-hash it again and compared the hashes.

Once the hashed password is obtained, the user itself is stored in the database with all 4 incoming arguments except the password which is the hashed version.

Login Function

The login function takes in the username and the incoming plain password and verifies that the hashed version corresponds with the one stored in the database.

An important point here is that this function returns a promise in all cases. If the username doesn't exist or the password is blank, a false response is returned in a promise. Otherwise, the response of bcrypt's compare function is returned. This function returns true if the passwords match, false if they do not.

Defining some app settings

There's a few configuration items we need to take care of. First off, the session settings:

Then we need to setup how Koa will handle rendering EJS pages. This is one pretty awesome thing about Koa, that this can be setup automatically and globally, but don't let me gush all over this!

Basic Routes

So let's establish our "routes", which is the pages that can be accessed by the browser. With the help of the Router, this can be really straightforward.

Then we have the login route, which does a lot of the bulk of our work. It checks for login, and adds everything it needs to the session in Koa if the authentication is successful:

Let's also create a logout function, that simply destroys the current session and returns the user to the index:

This one is pretty straightforward, so I don't think I need to get into the details, right? ;)

Lastly, we have the route for our "private" page. the one that only works if you're logged in. Now, there are "better" ways to establish protected routes, but let's go with the simplest one for now. We're just going to check for the logged property of the session to determine if the user is logged in.

The End of the File

At the very end of our file we still have a bit of stuff to add. Mainly starting the server, but also telling the parser and routers to initialize. This would be how it's done:

Creating Templates

While templating is slightly beyond the scope of an authentication tutorial, I would be remiss to ignore the fact that logging in without a page would be... let's say a little hard.

Koa's EJS templating configuration, that we did above, means that templates need to appear in the views folder. There will be a few template files:

  • template.html will be the "main" template. It will have the header, footer, and whatever else we want to appear on every page.

  • index.html will be the main page everyone can access.

  • login.html contains the login page and form

  • secret.html has a little secret about something.

Let's start with the template.html file.

The <%- body %> tag is where the contents of the other pages appear.

The index.html is just some welcome thingy that we aren't concerned about:

Then we have the login.html page which has our form. The form simply posts back to itself, so it'll trigger the .post endpoint:

And, finally, the secret.html page we've all been waiting for. Nothing that you haven't heard before, though:

With all said and done, . Want a more "advanced" version of this project? Check out .

npm i enmap better-sqlite-pool koa koa-session koa-ejs koa-bodyparser koa-router bcrypt
// Native Imports
const { sep, resolve, join } = require("path");

// Enmap Imports
const Enmap = require("enmap");
const users = new Enmap({ name: "users" });

// Bcrypt's hashing system
const bcrypt = require("bcrypt");

// Koa Imports
const Koa = require("koa");
// Koa's EJS renderer for HTML views.
const render = require("koa-ejs");
// The Body Parser to accept incoming form data and file uploads.
const parser = require("koa-bodyparser");
// The default "sessions" support.
const session = require("koa-session");
// Koa's get/post/etc router to simplify routes.
const Router = require("koa-router");
// Initializing the main components.
const router = new Router();
const app = new Koa();

// Define the data directory for templates (views).
const dataDir = resolve(`${process.cwd()}${sep}`);
const newuser = (username, name, plainpw, admin = false) => {
  if (users.has(username)) throw Error(`User ${username} already exists!`);
  bcrypt.hash(plainpw, 10, (err, password) => {
    if (err) throw err;
    users.set(username, {
      username, name, password, admin, created: Date.now()
    });
  });
};
const login = (username, password) => {
  const user = this.users.get(username);
  if (!user) return new Promise(resp => resp(false));
  if (!password) return new Promise(resp => resp(false));
  return bcrypt.compare(password, user.password);
};
app.keys = ['some secret hurr'];
app.use(session(app));
render(app, {
  root: join(__dirname, 'views'),
  layout: 'template',
  viewExt: 'html',
  cache: false,
  debug: true
});
router.get("/", async (ctx, next) => {
  // This is our static index page. It will render the views/index.js file
  await ctx.render("index");
});
// we obviously need a route to show the login page itself, too!
router.get("/login", async (ctx) => {
  await ctx.render("login");  
});

router.post("/login", async (ctx) => {
  // Fail if there is no username and password. 
  // This relies on koa-bodyparser
  if (!ctx.request.body.username || !ctx.request.body.password) {
    ctx.throw(400, "Missing Username or Password");
  }
  // Use our login function to verify the username/password is correct
  const success = await login(ctx.request.body.username, ctx.request.body.password);
  if (success) {
    // get the user's information
    const user = users.get(ctx.request.body.username);
    // Set all our session parameters:
    ctx.session.logged = true;
    ctx.session.username = ctx.request.body.username;
    ctx.session.admin = user.admin;
    ctx.session.name = user.name;
    // Save the session itself. This sets the cookie in the browser, 
    // as well as save into the sessions in memory.
    ctx.session.save();
    console.log(`User authenticated: ${user.username}`);
    // Once logged in, redirect to the secret page.
    ctx.redirect("/secret");
  } else {
    console.log("Authentication Failed");
    // Throw if the above login returns false.
    ctx.throw(403, "Nope. Not allowed, mate.");
  }
});
router.get("/logout", async (ctx) => {
  ctx.session = null;
  ctx.redirect("/");
});
router.get("/secret", async (ctx) => {
  if(!ctx.session.logged) ctx.throw(403, "Unauthorized to view this page");
  await ctx.render("secret");
});
app
  .use(parser())
  .use(router.routes())
  .use(router.allowedMethods());

app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

app.listen(3000);
views/template.html
<!DOCTYPE html>
<html lang="en">
	<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<title>A Page</title>
	</head>
  <body>
    <nav>
     <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/login">Login</a></li>
      <li><a href="/secret">Secret</a></li>
     </ul>
    </nav>
  <%- body %>
  </body>
</html>
views/index.html
<h1>Index</h1>
<p>Lorem Ipsum Carrots</p>
views/login.html
<h1>Login</h1>
<form method="POST">
 <p>Username: <input type="text" name="username" id="username"></p>
 <p>Password: <input type="password" name="password" id="password"></p>
 <p><button type="submit">Login</button></p>
</form>
views/secret.html
<h1>Some Secret</h1>
<p>Han Shot First.</p>
This should be your project
My full blogging platform, Koarrots

D.js Mod Logs

This page describes using 2 different enmaps in tandem using autonum and a reference array.

An interesting problem in javascript is that having an array of objects can be quite the ordeal. A lot of things you want to do require functions and loops, bleh. So, where Enmap is meant to be easier to use, this is an area where it's still a bit hard to handle things.

But there's a solution. If Enmap isn't enough, how about **TWO ** Enmap???? So yeah, we're going to be using one enmap to store user data, and another to store "warnings", that is to say, moderation actions stored as objects.

When we add a new element to the actions enmap, we'll be adding a reference to that new entry in the user enmap, via the autonum feature.

If you've read about databases a bit, you might have heard about "autonum" and "automatic indexes" before, and this is exactly it. Alright let's get down to brass tax!

The Actions Enmap

The actions enmap will be using autonum to generate new unique numbers that we'll be able to reference later. The setup is very typical, to begin. We'll attach these things to the discord.js client to keep things simple.

client.modActions = new Enmap({
    name: 'actions'
});

When we want to create a new action, it's a simple act of using autonum to get a key automatically. Let's do a simple warning:

const newActionId = client.modActions.autonum;
client.modActions.set(newActionId, {
    user: target.id,
    guild: message.guild.id,
    type: 'warning',
    moderator: message.author.id,
    reason: "Don't do it again!",
    when: Date.now()
});

So what this does is twofold: it gives us an ID, as well as save the data for this new warning in the Enmap.

The User Enmap

You might already have one of those enmaps lying around, but if you don't, the deal's pretty much the same (because enmap is simple!):

client.userProfiles = new Enmap({
    name: 'userProfiles'
});

We of course need to have some properties in there, and this will be done using ensure(). This is very similar to our Points system, and it can be done on user join (guildMemberAdd) and/or in the message event. Both would be fine:

client.userProfiles.ensure(message.author.id, {
    id: message.author.id,
    guild: message.guild.id,
    totalActions: 0,
    warnings: [],
    kicks: []
});

The Commands

So now we have everything ready to create a simple warn command that will use the above setup to create what we need:

if (command === 'warn') {
    // get a target user
    const target = message.mentions.users.first();
    // remove the mention from the message, join for a reason
    const reason = args.slice(1).join(" ");
    const newActionId = client.modActions.autonum;
    client.modActions.set(newActionId, {
        user: target.id,
        guild: message.guild.id,
        type: 'warning',
        moderator: message.author.id,
        reason: reason,
        when: Date.now()
    });
    // Push the action to the user's warnings
    client.userProfiles.push(target.id, newActionId, 'warnings');
    client.userProfiles.inc(target.id, 'totalActions');
    // then send some message or embed or whatever
    message.channel.send(`${target} was warned for '${reason}'`);
}

So how does this help us in the end? If you look at the warnings, you only get a bunch of IDs, right? Well, we can most definitely do some array magic in order to get these proper values... Yeah let's do that. Abracadabra!

if (command === 'mywarns') {
    const warnIDs = client.userProfiles.get(message.author.id, 'warnings');
    const warnData = warnIDs.map(id => client.modActions.get(id));
    // have fun displaying this wooh!
    message.reply(`You have ${warnIDs.length} warns, buddy!`);
}

Now go have fun an explore the endless possibilities of this system :D

Enmap and Josh

Let's take a quick peek at what Josh is, and what it means for the future of Enmap

As I've noted in my previous blog post, when Enmap moved to SQLite only, there were a few feathers and features lost in transition. Most notably, the loss of Providers was a big one, even though in my opinion it was a good trade-off to get the new features I wanted to include in Enmap 4 and onward.

But since that moment where Providers were removed, I had a plan in mind to give those that needed them an escape route. And not only that, Enmap itself does have some pretty solid limitations when it comes to growth, because of its lack of ability to support multiple processes and sharded applications.

Introducing Josh

Then plan was Josh, all along. Josh is the Javascript Object Storage Helper, and if that sounds a lot like what Enmap does it's because it is. In fact, Josh could best be described to you, my reader, as "A version of Enmap that doesn't have caching, is promised-based, and supports providers again".

So I've been working on it for a few years now - not full time, mind you, as it would have been ready a long time ago, but as a side project. It's finally picked up steam, and you can Get Josh right now to try out the early access version. It's limited (not as powerful as Enmap is currently) but that's rapidly evolving.

So what does that mean for Enmap?

You might immediately wonder, "But Evie, if you're working on Josh, what's going to happen with Enmap?" and I'm telling you right now, you don't need to worry about this. Enmap is still growing in popularity, I still have things to do with it, and I fully intend on maintaining and enhancing it in the future.

Josh might be similar to Enmap but it's not made to replace it! It has a different purpose, which is to support larger applications, potentially web-based ones, provide live updates, and all the things that were lost with Enmap's great provider purge. And since Josh is promise-based, it's not as simple to pick up as Enmap was, so I do expect people to start off with Enmap either way.

Josh and Enmap should, and will, be fully compatible with one another, in that you will be able to easily migrate between them (with export() and import() ), and moving from one to another would require a minimal amount of code changes. It's not zero, but I'm trying as much as possible to keep those differences as small as possible.

What does the future hold?

I've already back-ported a few things that I originally intended for Josh as part of Enmap's new updates. The observe() method, as well as the serializer/deserializer feature, were originally intended for Josh but ended up being implementable in Enmap also. This means, if I add a feature to Josh, I will add it to Enmap if I can, if it's compatible. So you won't be left behind!

It is my sincere hope that Enmap and Josh will both continue to grow, to help more people, and to help us all create better code, together!

Why SQLITE only?

This page explains the reason behind the removal of the "Provider" system, and the selection of sqlite as the only database available for Enmap starting version 4

Why providers in the first place?

So one of the major changes from Enmap 3 to 4 is the removal of Providers. Providers were something I've had since Enmap 1.0 (when I converted from djs-collections-persistent), and had 2 advantages (2 reasons to have them in the first place).

  1. It enabled supporting more databases, not only one. This gave more power to users, and, I thought, more capabilities.

  2. It separated the memory enmap (non-persistent) from the database layer, so installing enmap didn't require installing sqlite.

And why remove them?

But, after a year of updating Enmap, I realized that I'd painted myself in a corner with Providers. There came to light that there were multiple limitations to providers:

  1. Features were limited to the "lowest common denominator", whatever was available to all providers. For instance, better-sqlite3 is a synchronous module that's nonblocking (which is a magical thing, really). But since all other providers required promises, then I had to use sqlite as a promise module.

  2. Maintaining multiple providers is hard work. Every new feature would require updating all the providers (5 at this time), and there were many requests to create new providers which is an annoying, sometimes complicated task that adds even more work in the future.

  3. There were features I wanted that simply weren't possible, physically, with the providers (like the fetchAll/autoFetch options).

In addition, the advantages became lesser with time. I realized most people were using leveldb at first, then most switch to sqlite when I updated guides to use that provider. Essentially, most people use whatever they're told to use. So, just forcing one database wasn't that much of an issue and didn't affect the majority of users.

Also, most people did use enmap with persistence, and those that didn't... well, most users have enmap to use with discord.js bots in the first place which gives them Collection - almost the same as a non-persistent enmap.

What are the advantages of sqlite?

The reasoning behind removing all other providers and keeping sqlite was for specific features and capabilities inherent to the module I'm using, better-sqlite3.

  • better-sqlite3 is, as I mention, synchronous , which means, no callbacks, no promises. Just straight-up "make a request and it does it before the next line". No more need for "waiting" for things, resolving promises, etc.

  • The sync nature of better-sqlite3 means I can add an autoFetch feature. I can simply say "If the key isn't there, try to get the data", without requiring the user to resolve a promise. This is awesome.

  • By the same token, I can also add simple things like "get all keys in the database" using a getter. This means you can do enmap.indexes and this is actually querying the database seamlessly without the user really knowing it does that. Same for enmap.count and other features I'm planning.

So overall, I'm happy with my decision. It gives me more power, it gives users more features, and the people affected by the removal of the other providers are few and far between. Hell, enmap-pgsql has less than 1000 downloads on npm which is mostly mirrors and caches. It showed me that provider was pretty useless in the first place.

But what about people that NEED a provider?

I recognize that some people might want to use enmap and can't use sqlite. This is for many valid reasons, for example using it on heroku which doesn't support sqlite and leveldb. For those users, I'm keeping the providers open for maintenance. If someone wants to maintain and update the V3 branch, or even fork the entire system and maintain it under a new name, I have no issue with that (assuming licenses are properly kept). I'll accept PRs on all enmap repositories, including backporting some features and adding new ones.

I'm also keeping the V3 docs in this gitbook so it can be maintained through gitbook and PRed on github.

You can still install any provider as you would before, and install enmap using npm i eslachance/enmap#v3 for the version 3 branch that will remain.

Want to help out? Join the Discord, fork and PR the Github for enmap, contribute!

Enmap's History

From the first moment where I started using the Discord.js library, one thing in it fascinated me: "Collections". Discord.js Collections are a Map structure from JavaScript on top of which a bunch of useful methods are added, most notably those from JavaScript's Array structure.

Things like map, filter, reduce, find, sort... they made Maps so useful, so much more powerful, that I admired their design. It struck me at one point, that if such a structure were to be separated from Discord.js and perhaps made to be saved in a database, it would make interacting with data so easy that even a child could do it.

So when I started getting seriously into bot that required their own data to be saved, I turned to Amish Shah (Hydrabolt) and I said to him, I said "Listen, buddy, can I extract Collections and publish them as a separate module? That'd be awesome!" and Amish replied, like the great guy he is, "uhhhh sure, why not?"

And so, in May 2017, the djs-collection module was born. It was a simple thing, just straight-up lifted from Discord.js' code (not illegally, mind you, I retained all proper licenses and credits to Hydrabolt!). The following month, I added persistence (saving to a database) to it and published djs-collection-persistent , which then became my own defacto way to save data to a database.

But... let's be honest, npm install --save djs-collection-persistent is a mouthful to type out. Plus, I was realizing that having those two as separate modules meant I had to update them separately and ensure they still worked individually... So at one point, I decided it was time to merge them.

Releasing a single repository meant that I could now change the name, because of the aformentioned "omg mile-long name" problem, and just the overall annoyance of writing it. So I settled on "well, they're enhanced maps, let's call it Enmap!". A quick search revealed Enmap's only other apparent meaning was that it was the name of a satellite, and I was guessing no one would confuse the two.

But I didn't want to force enmap users to have persistence, so at the same time I actually created a separate module called enmap-level, which controlled the database layer and was completely optional. These modules I called Providers since, obviously, they provided data persistence and and API.

Enmap 0.4.0 was released at the beginning of October 2017, and since then has grown into a fairly solid module used by tens of thousands of people across the world, not only in discord.js bots but also in other projects built with Node. Its solidity and simplicity makes it the ideal storage for simple key/value pairs, and I'm extremely proud to have made it.

At the moment of writing this (2018-09-02) Enmap has been downloaded over 32,000 times and is growing by the minute with almost 10,000 downloads in August alone!

Update, August 2019

It's been a year now since I last wrote this post. And in case you were wondering, growth hasn't stopped! In fact, it's quite accelerated. One big change that I made was to go back to a single locked-in database provider, which I describe in Why SQLite Only?

Other than that, and adding some new features due to the switch to better-sqlite3, the main event is that just las month I reached a whopping 500,000 downloads for Enmap. Yes, that's a half million downloads for my little useful module that I started making for myself and ended up being useful for so many.

Full Documentation

The complete and unadultered API documentation for every single method and property accessible in Enmap.

The following is the complete list of methods available in Enmap. As it is auto-generated from the source code and its comments, it's a little more "raw" than the Usage docs. However, it has the benefit of being more complete and usually more up to date than the manually written docs.

If you're doing a PR on the docs github, please do not manually edit the below contents, as it will be overwritten. Check the src/index.js source code and change the comments there instead!

new Enmap(iterable, [options])

Initializes a new Enmap, with options.

Param

Type

Default

Description

iterable

iterable | string

If iterable data, only valid in non-persistent enmaps. If this parameter is a string, it is assumed to be the enmap's name, which is a shorthand for adding a name in the options and making the enmap persistent.

[options]

Object

Additional options for the enmap. See for details.

[options.name]

string

The name of the enmap. Represents its table name in sqlite. If present, the enmap is persistent. If no name is given, the enmap is memory-only and is not saved in the database. As a shorthand, you may use a string for the name instead of the options (see example).

[options.fetchAll]

boolean

Defaults to true. When enabled, will automatically fetch any key that's requested using get, or other retrieval methods. This is a "syncroneous" operation, which means it doesn't need any of this promise or callback use.

[options.dataDir]

string

Defaults to ./data. Determines where the sqlite files will be stored. Can be relative (to your project root) or absolute on the disk. Windows users , remember to escape your backslashes!

[options.cloneLevel]

string

Defaults to deep. Determines how objects and arrays are treated when inserting and retrieving from the database. See for more details on this option.

[options.ensureProps]

boolean

defaults to true. If enabled and the value in the enmap is an object, using ensure() will also ensure that every property present in the default object will be added to the value, if it's absent. See ensure API reference for more information.

[options.autoEnsure]

*

default is disabled. When provided a value, essentially runs ensure(key, autoEnsure) automatically so you don't have to. This is especially useful on get(), but will also apply on set(), and any array and object methods that interact with the database.

[options.autoFetch]

boolean

defaults to true. When enabled, attempting to get() a key or do any operation on existing keys (such as array push, etc) will automatically fetch the current key value from the database. Keys that are automatically fetched remain in memory and are not cleared.

[options.serializer]

function

Optional. If a function is provided, it will execute on the data when it is written to the database. This is generally used to convert the value into a format that can be saved in the database, such as converting a complete class instance to just its ID. This function may return the value to be saved, or a promise that resolves to that value (in other words, can be an async function).

[options.deserializer]

function

Optional. If a function is provided, it will execute on the data when it is read from the database. This is generally used to convert the value from a stored ID into a more complex object. This function may return a value, or a promise that resolves to that value (in other words, can be an async function).

[options.wal]

boolean

false

Check out Write-Ahead Logging:

Example

const Enmap = require("enmap");
// Non-persistent enmap:
const inMemory = new Enmap();

// Named, Persistent enmap with string option
const myEnmap = new Enmap("testing");

// Enmap that does not fetch everything, but does so on per-query basis:
const myEnmap = new Enmap({name: "testing", fetchAll: false});

// Enmap that automatically assigns a default object when getting or setting anything.
const autoEnmap = new Enmap({name: "settings", autoEnsure: { setting1: false, message: "default message"}})

enmap.count ⇒ integer

Retrieves the number of rows in the database for this enmap, even if they aren't fetched.

Kind: instance property of Enmap Returns: integer - The number of rows in the database.

enmap.indexes ⇒ array.<string>

Retrieves all the indexes (keys) in the database for this enmap, even if they aren't fetched.

Kind: instance property of Enmap Returns: array.<string> - Array of all indexes (keys) in the enmap, cached or not.

enmap.autonum ⇒ number

Generates an automatic numerical key for inserting a new value. This is a "weak" method, it ensures the value isn't duplicated, but does not guarantee it's sequential (if a value is deleted, another can take its place). Useful for logging, actions, items, etc - anything that doesn't already have a unique ID.

Kind: instance property of Enmap Returns: number - The generated key number. Example

enmap.set(enmap.autonum, "This is a new value");

enmap.set(key, val, path) ⇒ Enmap

Sets a value in Enmap.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

Required. The key of the element to add to The Enmap.

val

*

Required. The value of the element to add to The Enmap. If the Enmap is persistent this value MUST be stringifiable as JSON.

path

string

null

Optional. The path to the property to modify inside the value object or array. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

Example

// Direct Value Examples
enmap.set('simplevalue', 'this is a string');
enmap.set('isEnmapGreat', true);
enmap.set('TheAnswer', 42);
enmap.set('IhazObjects', { color: 'black', action: 'paint', desire: true });
enmap.set('ArraysToo', [1, "two", "tree", "foor"])

// Settings Properties
enmap.set('IhazObjects', 'blue', 'color'); //modified previous object
enmap.set('ArraysToo', 'three', 2); // changes "tree" to "three" in array.

enmap.update(key, valueOrFunction) ⇒ *

Update an existing object value in Enmap by merging new keys. This only works on objects, any other value will throw an error. Heavily inspired by setState from React's class components. This is very useful if you have many different values to update and don't want to have more than one .set(key, value, prop) lines.

Kind: instance method of Enmap Returns: * - The updated, merged value.

Param

Type

Description

key

string

The key of the object to update.

valueOrFunction

*

Either an object to merge with the existing value, or a function that provides the existing object and expects a new object as a return value. In the case of a straight value, the merge is recursive and will add any missing level. If using a function, it is your responsibility to merge the objects together correctly.

Example

// Define an object we're going to update
enmap.set("obj", { a: 1, b: 2, c: 3 });

// Direct merge
enmap.update("obj", { d: 4, e: 5 });
// obj is now { a: 1, b: 2, c: 3, d: 4, e: 5 }

// Functional update
enmap.update("obj", (previous) => ({
  ...obj,
  f: 6,
  g: 7
}));
// this example takes heavy advantage of the spread operators.
// More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

enmap.get(key, path) ⇒ *

Retrieves a key from the enmap. If fetchAll is false, returns a promise.

Kind: instance method of Enmap Returns: * - The value for this key.

Param

Type

Default

Description

key

string

The key to retrieve from the enmap.

path

string

null

Optional. The property to retrieve from the object or array. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

Example

const myKeyValue = enmap.get("myKey");
console.log(myKeyValue);

const someSubValue = enmap.get("anObjectKey", "someprop.someOtherSubProp");

enmap.observe(key, path) ⇒ *

Returns an observable object. Modifying this object or any of its properties/indexes/children will automatically save those changes into enmap. This only works on objects and arrays, not "basic" values like strings or integers.

Kind: instance method of Enmap Returns: * - The value for this key.

Param

Type

Default

Description

key

*

The key to retrieve from the enmap.

path

string

null

Optional. The property to retrieve from the object or array.

enmap.fetchEverything() ⇒ Enmap

Fetches every key from the persistent enmap and loads them into the current enmap value.

Kind: instance method of Enmap Returns: Enmap - The enmap containing all values.

enmap.fetch(keyOrKeys) ⇒ Enmap | *

Force fetch one or more key values from the enmap. If the database has changed, that new value is used.

Kind: instance method of Enmap Returns: Enmap | * - The Enmap, including the new fetched values, or the value in case the function argument is a single key.

Param

Type

Description

keyOrKeys

string | number | Array.<(string|number)>

A single key or array of keys to force fetch from the enmap database.

enmap.evict(keyOrArrayOfKeys) ⇒ Enmap

Removes a key or keys from the cache - useful when disabling autoFetch.

Kind: instance method of Enmap Returns: Enmap - The enmap minus the evicted keys.

Param

Type

Description

keyOrArrayOfKeys

string | number | Array.<(string|number)>

A single key or array of keys to remove from the cache.

enmap.changed(cb)

Function called whenever data changes within Enmap after the initial load. Can be used to detect if another part of your code changed a value in enmap and react on it.

Kind: instance method of Enmap

Param

Type

Description

cb

function

A callback function that will be called whenever data changes in the enmap.

Example

enmap.changed((keyName, oldValue, newValue) => {
  console.log(`Value of ${keyName} has changed from: \n${oldValue}\nto\n${newValue}`);
});

enmap.close() ⇒ Promise.<*>

Shuts down the database. WARNING: USING THIS MAKES THE ENMAP UNUSEABLE. You should only use this method if you are closing your entire application. This is useful if you need to copy the database somewhere else, or if you're somehow losing data on shutdown.

Kind: instance method of Enmap Returns: Promise.<*> - The promise of the database closing operation.

enmap.push(key, val, path, allowDupes) ⇒ Enmap

Push to an array value in Enmap.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

Required. The key of the array element to push to in Enmap. This value MUST be a string or number.

val

*

Required. The value to push to the array.

path

string

null

Optional. The path to the property to modify inside the value object or array. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

allowDupes

boolean

false

Optional. Allow duplicate values in the array (default: false).

Example

// Assuming
enmap.set("simpleArray", [1, 2, 3, 4]);
enmap.set("arrayInObject", {sub: [1, 2, 3, 4]});

enmap.push("simpleArray", 5); // adds 5 at the end of the array
enmap.push("arrayInObject", "five", "sub"); // adds "five" at the end of the sub array

enmap.math(key, operation, operand, path) ⇒ Enmap

Executes a mathematical operation on a value and saves it in the enmap.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

The enmap key on which to execute the math operation.

operation

string

Which mathematical operation to execute. Supports most math ops: =, -, *, /, %, ^, and english spelling of those operations.

operand

number

The right operand of the operation.

path

string

null

Optional. The property path to execute the operation on, if the value is an object or array.

Example

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});

points.math("number", "/", 2); // 21
points.math("number", "add", 5); // 26
points.math("number", "modulo", 3); // 2
points.math("numberInObject", "+", 10, "sub.anInt");

enmap.inc(key, path) ⇒ Enmap

Increments a key's value or property by 1. Value must be a number, or a path to a number.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

The enmap key where the value to increment is stored.

path

string

null

Optional. The property path to increment, if the value is an object or array.

Example

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});

points.inc("number"); // 43
points.inc("numberInObject", "sub.anInt"); // {sub: { anInt: 6 }}

enmap.dec(key, path) ⇒ Enmap

Decrements a key's value or property by 1. Value must be a number, or a path to a number.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

The enmap key where the value to decrement is stored.

path

string

null

Optional. The property path to decrement, if the value is an object or array.

Example

// Assuming
points.set("number", 42);
points.set("numberInObject", {sub: { anInt: 5 }});

points.dec("number"); // 41
points.dec("numberInObject", "sub.anInt"); // {sub: { anInt: 4 }}

enmap.ensure(key, defaultValue, path) ⇒ *

Returns the key's value, or the default given, ensuring that the data is there. This is a shortcut to "if enmap doesn't have key, set it, then get it" which is a very common pattern.

Kind: instance method of Enmap Returns: * - The value from the database for the key, or the default value provided for a new key.

Param

Type

Default

Description

key

string

Required. The key you want to make sure exists.

defaultValue

*

Required. The value you want to save in the database and return as default.

path

string

null

Optional. If presents, ensures both the key exists as an object, and the full path exists. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

Example

// Simply ensure the data exists (for using property methods):
enmap.ensure("mykey", {some: "value", here: "as an example"});
enmap.has("mykey"); // always returns true
enmap.get("mykey", "here") // returns "as an example";

// Get the default value back in a variable:
const settings = mySettings.ensure("1234567890", defaultSettings);
console.log(settings) // enmap's value for "1234567890" if it exists, otherwise the defaultSettings value.

enmap.has(key, path) ⇒ boolean

Returns whether or not the key exists in the Enmap.

Kind: instance method of Enmap

Param

Type

Default

Description

key

string

Required. The key of the element to add to The Enmap or array. This value MUST be a string or number.

path

string

null

Optional. The property to verify inside the value object or array. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

Example

if(enmap.has("myKey")) {
  // key is there
}

if(!enmap.has("myOtherKey", "oneProp.otherProp.SubProp")) return false;

enmap.includes(key, val, path) ⇒ boolean

Performs Array.includes() on a certain enmap value. Works similar to Array.includes().

Kind: instance method of Enmap Returns: boolean - Whether the array contains the value.

Param

Type

Default

Description

key

string

Required. The key of the array to check the value of.

val

string | number

Required. The value to check whether it's in the array.

path

*

Required. The property to access the array inside the value object or array. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

enmap.delete(key, path) ⇒ Enmap

Deletes a key in the Enmap.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

Required. The key of the element to delete from The Enmap.

path

string

null

Optional. The name of the property to remove from the object. Can be a path with dot notation, such as "prop1.subprop2.subprop3"

enmap.clear()

Deletes everything from the enmap. If persistent, clears the database of all its data for this table.

Kind: instance method of Enmap

enmap.destroy() ⇒ null

Completely destroys the entire enmap. This deletes the database tables entirely. It will not affect other enmap data in the same database, however. THIS ACTION WILL DESTROY YOUR DATA AND CANNOT BE UNDONE.

Kind: instance method of Enmap

enmap.remove(key, val, path) ⇒ Enmap

Remove a value in an Array or Object element in Enmap. Note that this only works for values, not keys. Note that only one value is removed, no more. Arrays of objects must use a function to remove, as full object matching is not supported.

Kind: instance method of Enmap Returns: Enmap - The enmap.

Param

Type

Default

Description

key

string

Required. The key of the element to remove from in Enmap. This value MUST be a string or number.

val

* | function

Required. The value to remove from the array or object. OR a function to match an object. If using a function, the function provides the object value and must return a boolean that's true for the object you want to remove.

path

string

null

Optional. The name of the array property to remove from. Can be a path with dot notation, such as "prop1.subprop2.subprop3". If not presents, removes directly from the value.

Example

// Assuming
enmap.set('array', [1, 2, 3])
enmap.set('objectarray', [{ a: 1, b: 2, c: 3 }, { d: 4, e: 5, f: 6 }])

enmap.remove('array', 1); // value is now [2, 3]
enmap.remove('objectarray', (value) => value.e === 5); // value is now [{ a: 1, b: 2, c: 3 }]

enmap.export() ⇒ string

Exports the enmap data to a JSON file. WARNING: Does not work on memory enmaps containing complex data!

Kind: instance method of Enmap Returns: string - The enmap data in a stringified JSON format.

enmap.import(data, overwrite, clear) ⇒ Enmap

Import an existing json export from enmap from a string. This data must have been exported from enmap, and must be from a version that's equivalent or lower than where you're importing it.

Kind: instance method of Enmap Returns: Enmap - The enmap with the new data.

Param

Type

Default

Description

data

string

The data to import to Enmap. Must contain all the required fields provided by export()

overwrite

boolean

true

Defaults to true. Whether to overwrite existing key/value data with incoming imported data

clear

boolean

false

Defaults to false. Whether to clear the enmap of all data before importing (WARNING: Any exiting data will be lost! This cannot be undone.)

enmap.array() ⇒ Array

Creates an ordered array of the values of this Enmap. The array will only be reconstructed if an item is added to or removed from the Enmap, or if you change the length of the array itself. If you don't want this caching behaviour, use Array.from(enmap.values()) instead.

Kind: instance method of Enmap

enmap.keyArray() ⇒ Array.<(string|number)>

Creates an ordered array of the keys of this Enmap The array will only be reconstructed if an item is added to or removed from the Enmap, or if you change the length of the array itself. If you don't want this caching behaviour, use Array.from(enmap.keys()) instead.

Kind: instance method of Enmap

enmap.random([count]) ⇒ * | Array.<*>

Obtains random value(s) from this Enmap. This relies on array.

Kind: instance method of Enmap Returns: * | Array.<*> - The single value if count is undefined, or an array of values of count length

Param

Type

Description

[count]

number

Number of values to obtain randomly

enmap.randomKey([count]) ⇒ * | Array.<*>

Obtains random key(s) from this Enmap. This relies on keyArray

Kind: instance method of Enmap Returns: * | Array.<*> - The single key if count is undefined, or an array of keys of count length

Param

Type

Description

[count]

number

Number of keys to obtain randomly

enmap.findAll(prop, value) ⇒ Array

Searches for all items where their specified property's value is identical to the given value (item[prop] === value).

Kind: instance method of Enmap

Param

Type

Description

prop

string

The property to test against

value

*

The expected value

Example

enmap.findAll('username', 'Bob');

enmap.find(propOrFn, [value]) ⇒ *

Searches for a single item where its specified property's value is identical to the given value (item[prop] === value), or the given function returns a truthy value. In the latter case, this is identical to Array.find().

All Enmap used in Discord.js are mapped using their `id` property, and if you want to find by id you should use the `get` method. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.

Kind: instance method of Enmap

Param

Type

Description

propOrFn

string | function

The property to test against, or the function to test with

[value]

*

The expected value - only applicable and required if using a property for the first argument

Example

enmap.find('username', 'Bob');

Example

enmap.find(val => val.username === 'Bob');

enmap.findKey(propOrFn, [value]) ⇒ string | number

Searches for the key of a single item where its specified property's value is identical to the given value (item[prop] === value), or the given function returns a truthy value. In the latter case, this is identical to Array.findIndex().

Kind: instance method of Enmap

Param

Type

Description

propOrFn

string | function

The property to test against, or the function to test with

[value]

*

The expected value - only applicable and required if using a property for the first argument

Example

enmap.findKey('username', 'Bob');

Example

enmap.findKey(val => val.username === 'Bob');

enmap.sweep(fn, [thisArg]) ⇒ number

Removes entries that satisfy the provided filter function.

Kind: instance method of Enmap Returns: number - The number of removed entries

Param

Type

Description

fn

function

Function used to test (should return a boolean)

[thisArg]

Object

Value to use as this when executing function

enmap.filter(fn, [thisArg]) ⇒ Enmap

Identical to Array.filter(), but returns a Enmap instead of an Array.

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function used to test (should return a boolean)

[thisArg]

Object

Value to use as this when executing function

enmap.filterArray(fn, [thisArg]) ⇒ Array

Identical to Array.filter().

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function used to test (should return a boolean)

[thisArg]

Object

Value to use as this when executing function

enmap.map(fn, [thisArg]) ⇒ Array

Identical to Array.map().

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function that produces an element of the new array, taking three arguments

[thisArg]

*

Value to use as this when executing function

enmap.some(fn, [thisArg]) ⇒ boolean

Identical to Array.some().

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function used to test (should return a boolean)

[thisArg]

Object

Value to use as this when executing function

enmap.every(fn, [thisArg]) ⇒ boolean

Identical to Array.every().

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function used to test (should return a boolean)

[thisArg]

Object

Value to use as this when executing function

enmap.reduce(fn, [initialValue]) ⇒ *

Identical to Array.reduce().

Kind: instance method of Enmap

Param

Type

Description

fn

function

Function used to reduce, taking four arguments; accumulator, currentValue, currentKey, and enmap

[initialValue]

*

Starting value for the accumulator

enmap.clone() ⇒ Enmap

Creates an identical shallow copy of this Enmap.

Kind: instance method of Enmap Example

const newColl = someColl.clone();

enmap.concat(...enmaps) ⇒ Enmap

Combines this Enmap with others into a new Enmap. None of the source Enmaps are modified.

Kind: instance method of Enmap

Param

Type

Description

...enmaps

Enmaps to merge

Example

const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl);

Enmap.multi(names, options) ⇒ Array.<Map>

Initialize multiple Enmaps easily.

Kind: static method of Enmap Returns: Array.<Map> - An array of initialized Enmaps.

Param

Type

Description

names

Array.<string>

Array of strings. Each array entry will create a separate enmap with that name.

options

Object

Options object to pass to each enmap, excluding the name..

Example

// Using local variables.
const Enmap = require('enmap');
const { settings, tags, blacklist } = Enmap.multi(['settings', 'tags', 'blacklist']);

// Attaching to an existing object (for instance some API's client)
const Enmap = require("enmap");
Object.assign(client, Enmap.multi(["settings", "tags", "blacklist"]));
https://enmap.evie.codes/usage#enmap-options
https://enmap.evie.codes/usage#enmap-options
https://www.sqlite.org/wal.html
Enmap

Supporters and Partners

The people that help make Enmap, and my other projects, possible!

Enmap Partners

An Idiot's Guide

My home for many years, way before Enmap was even a thought, AIG has been the place where I can be myself. That is, be sometimes of an asshole and sometimes helpful to beginners wanting to learn discord.js. Sometimes both at the same time!

AIG helps users with discord.js support for version 11 and 12, has a channel for Enmap, and is a community of builders. Notably, we're the authors of the famous An Idiot's Guide to Discord.js (if that wasn't obvious), where I'm the main author but there have been many contributions over the year. For discord.js support, head on over to https://anidiots.guide/ for the guide itself and https://discord.gg/vXVxsAjSMF to join us on Discord.

Enmap Supporters

AndrewJWin

The first supporter on GitHub and Patreon, working towards a degree in IT and an occasional idiot on An Idiots Guide.

He's Andrew()#9999 on Discord, and always willing to tell you if your nose looks weird.