ningilin v1.0.0
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 isgetPath()
+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,
}
4 years ago