arrow-left

All pages
gitbookPowered by GitBook
1 of 9

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

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.

hashtag
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 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.

hashtag
Examples

hashtag
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.

// the default serializer
const serializer = (data, key) => {
  return data;
};
// the default deserializer
const deserializer = (data, key) => {
  return data;
};
// 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!");

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.

hashtag
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.

hashtag
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.

hashtag
That's it!

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

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.

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

    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

    hashtag
    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:

    hashtag
    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!

    hashtag
    Deleting Data

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

    Serializing and Deserializing
    Working with Objects
    Array Methods
    <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");

    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 }]);

    hashtag
    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.

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

    hashtag
    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.push("simpleArray", 6);
    // now [1,2,3,4,5,6]
    
    myEnmap.push("arrInObj", "Robby", "aliases");
    // now ["Bobby", "Robert", "Robby"]
    Working With Objects
    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 }]

    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

    hashtag
    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.

    hashtag
    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.

    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
    });
    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.

  • 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:

    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:

    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.

    hashtag
    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).

    hashtag
    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:

    hashtag
    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:

    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, .

    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.

    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

    const myStructure = {
      first: "blah",
      second: "foo",
      changeme: "initial",
      isCool: false
      sub: {
        yay: true,
        thing: "amagig"
      }
    }
    you can see them here
    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.

    hashtag
    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:

    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.

    myEnmap.set("someObject", myStructure);
    
    // Or directly the object
    myEnmap.set("someObject", {first: "blah", ...});
    
    // Works with arrays, too!
    myEnmap.set("someArray", ["one", "two", "three"]);
    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}`);
    myEnmap.has("someObject", "sub.thing"); // returns true
    
    myEnmap.has("someObject", "heck"); // returns false.
    // Set an object property
    myEnmap.set("someObject", "newThing", "sub.blah");
    
    // Set an array property
    myEnmap.set("someArray", "four", 3);
    const myObject = {
      a: "foo",
      b: true,
      c: {
        but: "who",
        are: "you?",
        and: ["are you", "you?"],
      },
      sub: { values: { are: { "cool" } } },
    };
    myEnmap.set("myObject", {
      a: "foo",
      b: true,
      c: {
        but: "who",
        are: "you?",
        and: ["are you", "you?"],
      },
      sub: { values: { are: { "cool" } } },
    });

    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.

    hashtag
    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 _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?

    hashtag
    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:

    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.

    triangle-exclamation

    Important Note: Do NOT override Discord.js' existing collections! That means, client.users, client.guilds, etc. - 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.

    hashtag
    The Module Method

    All things considered, 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 , modules are fairly straightforward. This is how I have done an Enmap shared module before:

    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:

    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:

    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:

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

    _instance _arrow-up-right
    See all the properties and methods for the Discord.js clientarrow-up-right
    modules arrow-up-right
    My JavaScript Guidearrow-up-right
    My Modules Guide arrow-up-right
    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);
    const Enmap = require("enmap");
    
    module.exports = {
      settings: new Enmap({
        name: "settings",
        autoFetch: true,
        fetchAll: false
      }),
      users: new Enmap("users"),
      tags: new Emmap({ name : "tags" })
    }
    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!"
    });
    const guildTags = db.tags.find(tag => tag.guild === message.guild.id);
    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);
      }
    }

    Mathematical Methods

    circle-exclamation

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

    Some quick docs:

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

    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.

    hashtag
    enmap.inc(key, [objectPath])

    hashtag
    enmap.dec(key. [objectPath])

    *, 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.

  • // 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");
    // Assuming
    points.set("number", 42);
    points.set("numberInObject", {sub: { anInt: 5 }});
     
    points.inc("number"); // 43
    points.inc("numberInObject", "sub.anInt"); // {sub: { anInt: 6 }}
    // Assuming
    points.set("number", 42);
    points.set("numberInObject", {sub: { anInt: 5 }});
     
    points.dec("number"); // 41
    points.dec("numberInObject", "sub.anInt"); // {sub: { anInt: 4 }}