1.0.0 • Published 4 years ago

ningilin v1.0.0

Weekly downloads
-
License
ISC
Repository
gitlab
Last release
4 years ago

Ningilin: The Expression of Mongoose

Purpose

The mean.js stack has made creating a REST server easier. However, many times we create CRUD resources that utilize the same code base. For example:

let userModel = model(new Schema({
    username: String,
    password: String,
}));

...

app.get("/:id", async (req, res, next) => {
    await result = userModel.getById(id);
    req.status(200).json(result);
    next();
})

// You get the picture...
...

We're basically doing the same operations (CREATE, READ, UPDATE, DELETE) for each object.

This violates the DRY principle (Don't Repeat Yourself).

Ningilin solves this by creating a sort-of middleware for express. It works like this:

[Client]    ========>     [express]
            request         |
                            |
                            V
                           [POST /:id]      =========>    postOne()
                                            (call hook)
                           [GET /:id]       =========>    getOne()
                                            (call hook)
                           [UPDATE /:id]    =========>    getOne()
                                            (call hook)
                           [DELETE /:id]    =========>    deleteOne()
                                            (call hook)     ||
                                                            ||
           response                                         ||
[Client]  <============     [express]       <===============++

Installation

To install with NPM:

npm i --save ningilin @types/ningilin

If @types/ningilin is not available yet, please follow the alternative directions here: https://gitlab.com/srcrr/ningilin_types.

Usage

While Ningilin is meant to be extensible, the basic usage looks like this (in TypeScript):

import NingilinModel from "ningilin";

let MyModel = new NingilinModel(

    "MyModel",                      // The name of the Ningilin (used for the path)

    mongoose.Schema({
        name: String,
        number: Integer,
    }),                             // The Mongoose schema (model will be generated)
);

app.use(MyModel.getRouter());

That's all folks. that's all there is to it. You now have the following paths available to you:

POST    /mymodel/:_id       # Creates an instance of MyModel
POST    /mymodels           # Batch creates instance of MyModel
GET     /mymodel/:_id       # Gets an instance of MyModel
GET     /mymodels           # Filters for instances of MyModel
PATCH   /mymodel/:_id       # Updates an instance of MyModel
DELETE  /mymodel/:_id       # Deletes an instance of MyModel.

But I Want to Change Something!

Perfect! That's the intention. There are several main Ningilin hooks:

  • createOne
  • createMany
  • readOne
  • readMany
  • updateOne
  • deleteOne

DO NOT OVERRIDE THESE UNLESS ABSOLUTELY NECESSARY

Instead, pre and post hooks are available and are passed in via the constructor.

Basic Pre- and Post- Hooks

Each pre and post hook takes the following signature:

(req : express.Request, res : express.Response) : Promise<void> => {}

With any of the actions listed above, the pre and post hooks before<hook> and after<hook> are available to be overwritten.

They're async functions. Becasue of this, whenever you do implement a hook, DON'T FORGET TO RETURN! Otherwise, the promise won't be fulfilled.

Special Hooks

There are a few special case hooks that can be overridden, too.

Info Hooks.

These are useful for overriding information passed to Mongoose or express. Info hooks are in the form of the following signature:

() : any => {}

The function takes no arguments and returns any type. The following methods are available:

  • getPath(). By default the Ningilin model name in lower case.
  • getPluralPath(). By default is getPath() + s
  • getModel(). Dangerous to use. Basically injects a custom Mongoose Model.
  • getIdField(). Default is '_id'

Error Handler

Just like Highlander, there can be only one, called handleError.

(err : Error, req : expressRequest, res : express.Response, statusCode?: number) => Promise<void>

By default if statusCode is given or res.status is set, it will return that status code and set the response data to the error message.

The only exception is status codes > 500. In that case the error messages are just printed to the console.

Example of Overriding Pre- and Post-Hooks

let MyModel = new NingilinModel(

        "MyModel",

        mongoose.Schema({
            name: String,
            number: Integer,
        }),

        {
            beforeCreateOne: async (req : Request, res : Response) : Promise<any> => {
                if (req.body.number == Number(13)) {
                    throw new Error("I don't like the number 13.");
                }
                return null;
            }
        }
    );

What's going on?

This will be called before the creation of MyModel. If the number is 13, we'll throw an error. The error is handled by the handleError hook.

Global pre- and post- hooks.

let MyModel = new NingilinModel(

    "MyModel",

    mongoose.Schema({
        name: String,
        number: Integer,
    }),

    null,       // remember: this is the hook overrides.
    null,       // The path to be used for server calls, e.g. "supercoolmodel"
    
    {   // Pre hooks to be called on EVERY request BEFORE the responsible hook.
        notifyAllAdmins(req : Request, res : Response) => Promise<any> {
            console.log("Not sure why you'd do this, but is cool.");
        }
    },

    {   // Post hooks to be called on EVERY request AFTER the responsible hook.
        notifyAllAdmins(req : Request, res : Response) => Promise<any> {
            console.log(`This request has been completed: ${req}`);
        }
    }
);

Ningilin Constructor

/**
 * Construct a new Ningilin Model.
 * @param name Name of the collection
 * @param schema Mongoose Schema associated with the collection
 * @param hooks Optional hooks to override for the Ningilin
 * @param path Path that will be generated for the router (default is lowercase `name`)
 * @param preHooks List of hooks that will be called before *any* operation
 * @param postHooks List of hooks that will be called after *any* operation
 * @param collection Collection for the model (not used)
 * @param skipInit Skip initialization (not used)
 */
constructor (name: string, schema: Schema,
            hooks?: {[index: string]: Callable},
            path? : string,
            preHooks? : Array<Hook>,
            postHooks ? : Array<Hook>,
            collection ? : string,
            skipInit ? : boolean)

Hooks Available for override

private HOOKS = {
    getPath: this.getPath,
    getPluralPath: this.getPluralPath,
    getModel: this.getModel,
    getIdField: this.getIdField,

    beforeConnect: this.beforeConnect,
    afterConnect: this.afterConnect,
    
    beforeDisconnect: this.beforeDisconnect,
    afterDisconnect: this.afterDisconnect,

    beforeCreateOne: this.beforeCreateOne,
    createOne: this.createOne,
    afterCreateOne: this.afterCreateOne,

    beforeCreateMany: this.beforeCreateMany,
    createMany: this.createMany,
    afterCreateMany: this.afterCreateMany,

    beforeReadOne: this.beforeReadOne,
    readOne: this.readOne,
    afterReadOne: this.afterReadOne,

    beforeReadMany: this.beforeReadMany,
    readMany: this.readMany,
    afterReadMany: this.afterReadMany,

    beforeUpdateOne: this.beforeUpdateOne,
    updateOne: this.updateOne,
    afterUpdateOne: this.afterUpdateOne,

    beforeDeleteOne: this.beforeDeleteOne,
    deleteOne: this.deleteOne,
    afterDeleteOne: this.afterDeleteOne,

    handleError: this.handleError,
}