Skip to content

Forget about maintaining routes files, use dynamic routes created from files in a folder

License

Notifications You must be signed in to change notification settings

clubedaentrega/lift-it

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lift It

Build Status Inline docs Dependency Status

Forget about maintaining routes files, use dynamic routes created from files in a folder

Install

npm install lift-it

Basic usage

// Lift the 'api' folder
let api = require('lift-it')('./api').lift()

// Execute the file ./api/item/create.js
api.run('item/create', {
	name: 'Water',
	value: 17
}, function (err, response) {
	// Always async
})

Lift-it scans recursively a folder for *.js files. Those files should export a handler function, executed when someones 'run' them. The only ignored file is errors.js.

An example of 'item/create':

// `body` is the argument passed by 'run' (in this case, `{name:'Water',value:17}`)
// `success` and `error` are callback functions, learn about it here:
// https://github.com/clubedaentrega/run-it
module.exports.handler = function (body, success, error) {
	// Item.create represents an async job
	Item.create(body, error(function (item) {
		// Answer with {id: '54bd55fc13e53ce1077415a2'}
		success({
			id: item.id
		})
	}))
}

Express example

This module can be easily used with express (or any other route-based web framework).

An example of simple JSON-based POST api:

// Express
let app = require('express')()
app.use(require('body-parser').json())

// Lift
let api = require('lift-it')('./api').lift()

// Set routes
api.actions.forEach(function (action) {
	// `action` is an object. See the docs bellow
	
	app.post(action.name, function (req, res) {
		
		// action.run(...) is the same as api.run(action.name, ...)
		action.run(req.body, function (err, response) {
			res.json(err ? {error: String(err)} : response)
		})
	})
})

Plugins

The base module will scan a folder and look for exported handler in *.js files. This base behaviour can be expanded with plugins.

The available bundled plugins are:

  • validate: validate the input with validate-fields
  • filters: add filters (like middlewares)
  • profile: enable profiling async jobs

Docs

require('lift-it')

A function with one parameter, the folder path to scan: require('lift-it')(folder). Returns a lifter object.

require('lift-it').lean()

Do a lean lift. See lifter.lean(). Returns a lifted instance

lifter.use(plugIn)

Add the given plugin to the lift process. Read more about available and custom plugins bellow

lifter.recursive

Whether the folder should be scanned recursively or not. The default value is true.

lifter.requireFile

A callback to handle requiring a file. This allows one to completly change the semantics of a required file. The default value is function (require, path) { return require(path) }.

lifter.profile

lifter.errorClass

lifter.enableErrorCode

Change settings of run-it. Must be changed only before lifter.lift() is called.

lifter.lift()

Run the lift process (sync), throws if any error happens. For each *.js file in the target folder, all plugins will be executed in the order they were 'use'd.

This returns a lifted object.

lifter.leanLift()

Run the lean lift process, that is, only look for file names. This will not require files, nor apply plugins.

This returns a lifted object with the lean flag on. No action can be executed on it.

This is useful if you just want to check action names, not execute them.

lifted.get(name)

Return an action by name. Return an action object (or null if not found).

lifted.run(name, ...args, callback)

Run the action with the given arguments. callback(err,...output) is executed when done. The action is executed in its own domain, see run-it for more info about that.

Throws if no action with the given name is found.

If profile is enabled, the last argument for callback is the profile data.

lifted.getRunner(name)

Return a run-it runner for the given action. Call runner.exec(...args, callback) to actually execute the action. See action.getRunner().

Throws if no action with the given name is found.

lifted.actions

An array of action objects

lifted.lean

A boolean. If true, this is a lean lift (see lifter.leanLift()), so no action can be executed.

action.path

File path, like '/api/item/create.js'

action.name

Action name, like 'item/create'. This is the relative path to the target folder, without the trailing '.js'.

action.filters

An array of filters, executed before the action handler. This is used by plugins.

action.postFilters

An array of post filters, executed after the action handler. This is used by plugins.

action.module

The result of require-ing the file. action.module.handler is the handler for this action.

action.profile

A boolean that flags whether profiling is enabled for this action.

action.run(...args, callback)

Run this action. This is the same as lifted.run(action.name, ...args, callback).

action.getRunner()

Return the run-it runner for the action. Call runner.exec(...args, callback) to actually execute the action.

This is useful to make some adhoc changes to the runner, like runner.runInfo(info).profile(true).exec(...args, callback). See docs on run-it.

Available plugins

Profile

If the require-d file exports profile (a boolean), this value will overwrite the global lift.profile. Get this plugin with require('lift-it').profile().

For example, this file will have profiling always on:

module.exports.profile = true
module.exports.handler = function (body, success, error) {
	// ...
}

Use:

let liftIt = require('lift-it'),
	lift = liftIt('./api')
lift.use(liftIt.profile())
let api = lift.lift()
api.run('item/create', item, function (err, response, profileData) {
	// ...
})

Validate

Add input validation with validate-fields. Get it with require('lift-it').validate(options).

options is an optional object with the properties displayed bellow (default values indicated):

options = {
	// The export property name
	exportName: 'fields',
	// Whether to error out if the file does not export it
	optional: false,
	// Check input or output
	direction: 'input',
	// Which argument to validate
	// The default will check the first
	position: 0,
	// A function to return a value if the desired input/output is not present
	getDefaultValue: function () {
		throw new Error('Insufficient number of arguments')
	},
	// If lifter.enableErrorCode is true, which code to use in error(code, msg)
	code: 101,
	// If lifter.enableErrorCode is true, which code to use in error(HTTPStatusCode, code, msg)
	HTTPStatusCode: 500,
	// A function to handle error cases
	errorHandler: function (action, value, err) {
		// `action` is an Action instance
		// `value` is the offending value
		// `err` is a lifter.errorClass instance
		// `err.code` is `options.code`
		throw err
	},
	// Let you define your own custom types
	// See doc on validate-fields module for that
	defineTypes: function (validate) {},
	// Options passed to schema.validate() of validate-fields
	options: {}
}

Example of a file using it:

module.exports.fields = {
	name: String,
	value: 'uint'
}
module.exports.handler = function (body, success, error) {
	// ...
}
let liftIt = require('lift-it'),
	lift = liftIt('./api')
lift.use(liftIt.validate())
let api = lift.lift()
api.run('item/create', {}, function (err) {
	// err is new Error('I was expecting a value in name')
})

This plugin will set action.module[exportName+'-schema'] to validate.parse(action.module[exportName]).

Filters

Add support for filters. Filters are functions executed sequentially before the handler. One example of their use is implementing authentication. Get it with require('lift-it').filters(folder).

Filter handlers are implemented and exported by files in the filters folder. Each of those files may export as many filters as you want.

See an example of a file implementing two filters:

module.exports = function (body, success, error) {
	// Check auth, read the DB, etc
	success('Some data I got elsewhere')
}
module.exports.double = function (body, success, error) {
	// Simply change the body input
	body.value *= 2
	success()
}

The 'item/create' action may use those filters this way:

module.exports.filters = ['myFilter', 'myFilter.double']
module.exports.handler = function (body, moreData, success, error) {
	// body.value will be doubled
	// moreData will be the string create by the first filter
	// ...
}

If this filter file path is 'filters/myFilter.js', the main file (the one that lifts everything) may be:

let liftIt = require('lift-it'),
	lift = liftIt('./api')
lift.use(liftIt.filters('./filters'))
let api = lift.lift()
api.run('item/create', {value: 10}, function (err, response) {
	// ...
})

Custom plugins

A plugin is a function like function (action, lifter) {}. That function is called once for every file that is found in the lifted folder. Creating your own plugin is that simple:

let myPlugin = function (action, lifter) {
	// Do something with the action, like checking something
	if (action.name.indexOf('drop')) {
		throw new Error('Sorry, we do not put up with dropping things...')
	}
	
	// Add a filter to every action to delay them by 1s
	action.filters.push(function (body, success) {
		setTimeout(success, 1e3)
	})
	
	// Add a post filter to check something
	action.postFilters.push(function (response, success) {
		if (typeof response.status !== 'string') {
			throw new Error('Response should have the status field')
		}
		success(response)
	})
}

Warn: the lifter object should not be modified. It should be treated as read-only.

About

Forget about maintaining routes files, use dynamic routes created from files in a folder

Resources

License

Stars

Watchers

Forks

Packages

No packages published