Skip to content

Module API

Ayden Panhuyzen edited this page Jan 2, 2023 · 37 revisions

Place 2.0 is great because it's open, but everyone has different needs and uses for it. A lot of configuration can be done in the config.js file, but after that, it's hard to keep up with custom code changes and more. That's why we created the module API.

Modules are loaded when the Place 2.0 server starts up. They can do things such as adding extra HTML to the templates, adding more routes, adding middleware to every request, creating new data models, and even extending existing data models with new methods and fields.

Table of Contents

Module Structure

Modules are folders inside of the modules directory of Place 2.0. Name it something so that they can be easily identified.

The module.json file

Every module must have a module.json file in its root directory. This instructs Place 2.0 on how to load your module. The most basic example of a module.json looks like this:

{
    "identifier": "co.dynastic.place.module.test",
    "main": "main.js",
    "name": "Test Module"
}

module.json API reference list

  • identifier: This identifier is unique to your module and is how Place 2.0 refers to it internally. Make sure no other modules have this identifier. (required)
  • main: The main file for your module. It should also be in the root directory of your module. We'll get into this more in the next section. (required)
  • name: A user-friendly name for your module that we can show the user if ever needed. (required)
  • priority: A number stating the priority in the loading order of all modules. A module with a higher priority gets loaded before a module with a lower priority. (default: 1)
  • routes: A list of any routes that need to be processed by Place. (default: none) - API Reference - Example
  • depends: A list of identifiers for modules that your module relies on to run properly.
  • conflicts: A list of identifiers for modules that your module cannot run alongside.
  • publicRoot: The root folder to serve your module's static files from. (default: /)

Your main module file

The main module file contains your module's main class. This is responsible for loading any other code you might need and initializing your module. It should be in your module's root directory and have the same name you specified in your module.json.

Use this boilerplate template of a main module file to get started:

class Module {
    constructor(app) {
        this.app = app;
        this.meta = require('./module');
    }

    // any other code you might need

}

module.exports = Module;

The main Place app object is passed in the constructor of your module, and it is created while Place loads (just before the server starts listening). You can do anything in there you need to initialize your module and modify the main Place app object.

Adding more to the templates with extensions

To make it easier to add HTML to integral places, we have added insertion points in our module files. It's easy to add onto a template using insertion points. The following insertion points are available for creating your extensions:

  • head: Inside the <head> attribute of every page. (note: there's a better way to add CSS and JS, in your main module file)
  • nav_left: Inside the navigation bar, to the left of all of the built-in links.
  • nav_inside: Inside the navigation bar, right after the Home link.
  • nav_right: Inside the navigation bar, to the right of all of the built-in links (rules, admin).
  • nav_user_left: Inside the navigation bar, to the left of the user menu.
  • nav_user_right: Inside the navigation bar, to the right of the user menu.
  • main: In the <body> of the main Place page.
  • body: In the <body> of every page.
  • place: In the Place <div>.
  • footer: Inside the footer, after the copyright information.

Adding your own HTML into these insertion points is really easy. To start, create a folder called view_extensions in your module's root. For every insertion point you want to hook, create a file with the name of your insertion point with a Pug extension (e.g., nav_inside.pug). Add any HTML as Pug code you want to your template file and Place 2.0 will automatically load it. No need to add any other code.

Pro tip: Template extensions follow the same loading order based on the priority specified in your module.json when choosing how to lay out. Modules with higher priorities will get inserted before modules with lower priorities.

Adding extra JS and CSS to the site

Modules also have an easy way to add stylesheets and scripts to the website. Inside your main module file again, the methods getCSSResourceList and getJSResourceList are available. You simply have to return an array of the resources you would like to add to the page. In addition, the request is passed along as a parameter so you can add your own logic for adding files.

Here's an example how you would utilize one of these methods to add a stylesheet:

class Module {
    // ...
    getCSSResourceList(req) {
        return ["/css/module.css"];
    }
}
// ...

Easy, right? But we referenced a stylesheet, how can we actually make it available so that the browser can load it? We use the public folder.

Heads up! Remember that each resource can significantly increase the load time of a page. Use the request object passed as a parameter to only load the resources required for each page and get rid of unnecessary resources where possible.

Serving static files

Serving static files with modules is as easy as making a folder and dropping files in it. No really, that's all you have to do.

Make a folder called public in your module's root directory. From there, you can add any file and it will be served when requested by the browser. Try and keep a similar directory structure to Place itself (separate folders for each resource type, such as css, js, and img).

Pro tip: Your module.json has a setting named publicRoot, which can let you start your public directory at a different root (e.g. you can set it to /important-stuff and all the resources in your public folder will be accessible with /important-stuff before them).

Registering more routes

You can easily create more routes in your module. Create a folder named routes in your module's root to get started. Then, using the routes property of your module.json, you can tell Place how to process them.

Like other routes, you simply create an Express router and add your routes to it, then pass a function returning that router as an export. This function will be called when loading the routes, with the main Place app object as a parameter for convenience, but it is also available as a parameter on each request (req.place). A second parameter on this function, "m" contains a middleware of stuff that Place 2.0 will automatically set up for your convenience, such as the moduleResponseFactory.

Example Router file

const express = require('express');

function MyRouter(app, m) {
    var router = express.Router();
    router.use(m); // set up special module route additions

    router.get('/test', function(req, res) {
        return res.send("Hello, world!");
    });

    return router;
}

MyRouter.prototype = Object.create(MyRouter.prototype);

module.exports = MyRouter;

Once you've created the code for your route, you need to tell Place how to load it. In your module.json file, add a routes array like so:

Example Router module information

{
    "routes": [
        {
            "path": "/",
            "file": "MyRouter.js"
        }
    ]
}

Route object API reference list

  • path: The prefix to all requests to this route. (for example, if this is set to /test and you have a route going to /hello, you can call /hello by going to /test/hello) (required)
  • file: The file containing the code for your custom route (the /routes directory is assumed and not needed). (required)

How do I render my own views?

Since the normal request responseFactory goes to the ../views folder, a special moduleResponseFactory is available on every request passed to your router. This module response factory has your module's views folder as a root. If you have a module file named test.pug inside your module's views folder, you could load it like so:

req.moduleResponseFactory.sendRenderedResponse("index")

Adding middleware

Middleware allows you to process each request to add data or respond before passing it off to the appropriate routes. With modules, you can easily add your own middleware. If a module implements the method processRequest, all requests will be automatically sent through that function before proceeding. In order to allow the next function to be used more easily, we made it return an array of middleware functions to send it through. Here's an example that adds a header to each request. Like with resource loading, the request is passed along as a parameter so you can customize the middleware that is used to process that request.

Here's an example that adds a header to every request.

class Module {
    // ...
    processRequest(req) {
        var myMiddleware = (req, res, next) => {
             res.header('Hello', 'world!');
             next();
        }
        return [myMiddleware];
    }
}
// ...

These function exactly like Express middleware, so you must call next() in order to continue processing the request (if appropriate).

Pro tip: Like template extensions, module middleware is also applied in the same loading order that extensions are loaded in, based on the module's priority. Modules with higher priorities will have middleware processed before modules with lower priorities. All module middleware is processed after essential Place middleware.

Adding new data models

Start by creating a models folder in the root of your module's directory. To create your own data model, just create a file like any other Mongoose data model and return one created with DataModelManager.registerModel. Here's an example of a model for a chat message (in the Place code itself). Once you do this, you can import the file elsewhere in your code and just use it from there.

Adding onto existing data models

You can add/modify both static and instance methods, as well as your own fields to existing data models in Place 2.0. Create a new folder in your module's root called model_extensions. This folder will contain your model extensions, which works like a basic overriding system. Create a file with the same name as the original model file. Start by telling plugin manager that you want to override a model. Because this will be loaded automatically at runtime by Place, make a function that accepts two parameters (the app and the override manager) so that you can get these accordingly. Then, when you're done, return the override manager. The manager lets you modify the model in a safe and easy way.

Model Extension Manager API Reference

  • statics: Contains all of the new static methods defined by your module.
  • methods: Contains all of the new instance methods defined by your module.
  • hookStatics: Contains all of the pre-existing static methods your module wants to hook.
  • hookMethods: Contains all of the pre-existing instance methods your module wants to hook.
  • fields: Contains all of the new fields defined by your module.

Here's an example that shows off all of the possibilities with this API:

function UserOverride(app, manager) {
    manager.schemaName = "User";

    // Lets start by adding a new field to the user model. We simply just add our own property to the fields property of manager.
    // We'll add a boolean field called 'isAlive' to the user model:
    manager.fields.isAlive = Boolean;

    // Next we'll add a simple method called 'revive' that brings our user back to life if they die:
    manager.methods.revive = function() {
        this.isAlive = true;
        return this.save();
    }

    // We can next make a static method to fetch all the users in our database who are dead:
    manager.statics.getAllDeadUsers = function() {
        return this.find({isAlive: false})
    }

    // That was easy. Now we've got a way to tell if a user is dead, a way to revive them, and a convenience method to fetch all the dead users. But what if we want to actually modify Place's default logic? That's where 'hookStatics' and 'hookMethods' come in.

    // Dead people can't place! Lets modify the 'canPlace' method of the user model so that it always returns false for dead users.
    manager.hookMethods.canPlace = function(app, orig) {
        // See that 'orig' parameter? That's the original method. You can call that to get the original result or continue on with the code. Lets use that to return the original result if our user is actually alive, but always return false is they have died.
        if(!this.isAlive) return false;
        return orig(app);
    }

    // Making fun of the dead isn't nice. Let's make it so that names with the word 'dead' in them are invalid. We can do this by hooking the static 'isValidUsername' method.
    manager.hookStatics.isValidUsername(username, orig) {
        // Notice again how we've passed 'orig' as a parameter so you can easily call the original method.
        if(username.includes("dead")) return false;
        return orig(username);
    }

    // Finally, return our manager so that Place can load these modifications.
    return manager;
}

module.exports = UserOverride;

More about hooking

As you can see in the example above, hooking is a dead (oh shit) simple way of changing the outcome of one of Place's pre-existing model methods. All hooks should have the exact same parameters that the original method had, so if the original method had three parameters, make sure you also have those three parameters. In addition, the original method will be tacked onto the end as an extra parameter. In this case, you can add a fourth parameter called 'orig' to capture the original method (or the next module hooking it).

Pro tip: Model extension overrides are applied in the order of priority, so that modules with a higher priority will execute their hooks before modules with a lower priority. 'orig' actually calls the next method in the hooking chain, whether it be the original, or another module's hook of that method.