Comparing version 0.2.1 to 1.0.0
var Jammin = module.exports = {} | ||
Jammin.API = require('./lib/api.js'); | ||
Jammin.Client = require('./lib/client.js'); | ||
Jammin.middleware = require('./lib/middleware.js'); |
@@ -9,3 +9,3 @@ var Async = require('async'); | ||
this.label = label; | ||
this.db = model; | ||
this.db = this.model = model; | ||
} | ||
@@ -27,3 +27,8 @@ | ||
var find = many ? self.db.find : self.db.findOne; | ||
var run = find.apply(self.db, [query]).select(options.select || '').populate(options.populate || ''); | ||
var run = find.apply(self.db, [query, jammin.projection]).select(options.select || '').populate(options.populate || ''); | ||
if (jammin.populate) { | ||
if (Array.isArray(jammin.populate)) run.populate(jammin.populate[0], jammin.populate[1]); | ||
else run.populate(jammin.populate); | ||
} | ||
if (jammin.select) run.select(jammin.select); | ||
if (jammin.sort) run.sort(jammin.sort); | ||
@@ -63,5 +68,6 @@ if (jammin.limit) run.limit(jammin.limit); | ||
self.db.findOne(query).exec(function(err, oldDoc) { | ||
if (err) return callback(err); | ||
if (err || !oldDoc) return callback(err, oldDoc); | ||
for (var key in doc) { | ||
oldDoc[key] = doc[key]; | ||
oldDoc.markModified(key); | ||
} | ||
@@ -95,4 +101,4 @@ oldDoc.save(callback); | ||
else if (sendResponse && (useMethod === 'get' || useMethod === 'post' || useMethod === 'put')) { | ||
if (many) thing = thing.map(function(t) { return t.toObject(TO_OBJ_OPTIONS) }); | ||
else thing = thing.toObject(TO_OBJ_OPTIONS); | ||
if (many) thing = thing.map(function(t) { return t.toJSON(TO_OBJ_OPTIONS) }); | ||
else thing = thing.toJSON(TO_OBJ_OPTIONS); | ||
if (options.mapItem) { | ||
@@ -99,0 +105,0 @@ if (many) thing = thing.map(options.mapItem); |
{ | ||
"name": "jammin", | ||
"version": "0.2.1", | ||
"version": "1.0.0", | ||
"description": "REST API Generator using Express and Mongoose", | ||
@@ -23,2 +23,3 @@ "main": "index.js", | ||
"cors": "^2.7.1", | ||
"mockgoose": "^5.0.10", | ||
"password-hash": "^1.2.2", | ||
@@ -25,0 +26,0 @@ "validator": "^3.39.0" |
157
README.md
@@ -0,32 +1,30 @@ | ||
# jammin | ||
## Installation | ||
```npm install jammin``` | ||
**Note: Jammin is still in development. The API is not stable.** | ||
## About | ||
Jammin is the fastest way to build REST APIs in NodeJS. It consists of: | ||
* A light-weight wrapper around [Mongoose](http://mongoosejs.com/) to expose database operations | ||
* A light-weight module wrapper to expose functions as API endpoints | ||
* A light-weight wrapper around [Mongoose](http://mongoosejs.com/) to perform database operations | ||
* An Express router to link database operations to HTTP operations | ||
Jammin is built for [Express](http://expressjs.com/) and is fully extensible via **middleware** to support things like authentication, sanitization, and resource ownership. | ||
In addition to performing database CRUD, Jammin can bridge function calls over HTTP. If you have a node module that communicates via JSON-serializable data, Jammin allows you to ```require()``` that module from a remote NodeJS client. See the Modules section for an example. | ||
Use ```API.addModel()``` to add an existing Mongoose model. You can attach HTTP routes to each model that will use ```req.params``` and ```req.query``` to query the database and ```req.body``` to update it. | ||
Jammin can also serve a [Swagger](http://swagger.io) specification, allowing your API to link into tools like [Swagger UI](http://petstore.swagger.io/) and [LucyBot](https://lucybot.com) | ||
## Quickstart | ||
## Usage | ||
### Database Operations | ||
Use ```API.define()``` to create Mongoose models. You can attach HTTP routes to each model that will use ```req.params``` and ```req.query``` to query the database and ```req.body``` to update it. | ||
```js | ||
var App = require('express')(); | ||
var Mongoose = require('mongoose'); | ||
var Jammin = require('jammin'); | ||
var API = new Jammin.API('mongodb://<username>:<password>@<mongodb_host>'); | ||
var PetSchema = { | ||
var PetSchema = Mongoose.Schema({ | ||
name: String, | ||
age: Number | ||
}; | ||
}); | ||
API.define('Pet', PetSchema); | ||
var Pet = Mongoose.model('Pet', PetSchema); | ||
API.addModel('Pet', Pet); | ||
API.Pet.get('/pets/:name'); | ||
@@ -46,34 +44,2 @@ API.Pet.post('/pets'); | ||
### Modules (beta) | ||
Use ```API.module()``` to automatically pass ```req.query``` and ```req.body``` as arguments to a pre-defined set of functions. | ||
This example exposes filesystem operations to the API client. | ||
```js | ||
var App = require('express')(); | ||
var Jammin = require('jammin'); | ||
var API = new Jammin.API(); | ||
API.module('/files', {module: require('fs'), async: true}); | ||
App.use('/v0', API.router); | ||
App.listen(3000); | ||
``` | ||
```bash | ||
> curl -X POST $HOST/v0/files/writeFile?path=hello.txt -d {"data": "Hello World!"} | ||
> curl -X POST $HOST/v0/files/readFile?path=hello.txt | ||
Hello World! | ||
``` | ||
Use ```Jammin.Client()``` to create a client of the remote module. | ||
```js | ||
var RemoteFS = new Jammin.Client({ | ||
module: require('fs'), | ||
basePath: '/files', | ||
host: 'http://127.0.0.1:3000', | ||
}); | ||
RemoteFS.writeFile('foo.txt', 'Hello World!', function(err) { | ||
RemoteFS.readFile('foo.txt', function(err, contents) { | ||
console.log(contents); // Hello World! | ||
}); | ||
}); | ||
``` | ||
## Documentation | ||
@@ -86,3 +52,3 @@ | ||
```js | ||
API.Pet.get('/pet/:name'); | ||
API.Pet.get('/pets/:name'); | ||
API.Pet.getMany('/pets') | ||
@@ -131,29 +97,2 @@ ``` | ||
### Modules (beta) | ||
Jammin allows you to expose arbitrary functions as API endpoints. For example, we can give API clients access to the filesystem. | ||
```js | ||
API.module('/files', {module: require('fs'), async: true}) | ||
``` | ||
Jammin will expose top-level functions in the module as POST requests. Arguments can be passed in-order as a JSON array in the POST body. Jammin also parses the function's toString() to get parameter names, allowing arguments to be passed via a JSON object in the POST body (using the parameter names as keys). Strings can also be passed in as query parameters. | ||
All three of the following calls are equivalent: | ||
```bash | ||
> curl -X POST $HOST/files?path=foo.txt&data=hello | ||
> curl -X POST $HOST/files -d '{"path": "foo.txt", "data": "hello"}' | ||
> curl -X POST $HOST/files -d '["foo.txt", "hello"]' | ||
``` | ||
See the Middleware section below for an example of how to more safely expose fs | ||
Jammin also provides clients for exposed modules. This allows you to bridge function calls over HTTP, effectively allowing you to ```require()``` modules from a remote client. | ||
This allows you to quickly containerize node modules that communicate via JSON-serializable data, e.g. to place a particularly expensive operation behind a load balancer, or to run potentially malicious code inside a sandboxed container. | ||
```js | ||
var RemoteFS = new Jammin.Client({ | ||
module: require('fs'), | ||
basePath: '/files', | ||
host: 'http://127.0.0.1:3000', | ||
}); | ||
``` | ||
### Middleware | ||
@@ -168,4 +107,22 @@ You can use middleware to intercept database calls, alter the request, perform authentication, etc. | ||
Change ```req.jammin.arguments``` to alter function calls made to modules. | ||
Jammin also comes with prepackaged middleware to support the following Mongoose operations: | ||
`limit`, `sort`, `skip`, `projection`, `populate`, `select` | ||
#### Examples | ||
Here are three equivalent ways to sort the results and limit how many are returned. | ||
```js | ||
var J = require('jammin').middleware | ||
// The following are all equivalent | ||
API.Pet.getMany('/pets', J.limit(20), J.sort('+name')); | ||
API.Pet.getMany('/pets', J({limit: 20, sort: '+name'})); | ||
API.Pet.getMany('/pets', function(req, res, next) { | ||
req.jammin.limit = 20; | ||
req.jammin.sort = '+name'; | ||
next(); | ||
}) | ||
``` | ||
The example below alters ```req.query``` to construct a complex Mongo query from user inputs. | ||
@@ -197,44 +154,32 @@ ```js | ||
var setOwnership = function(req, res, next) { | ||
req.jammin.document.owner = req.user.username; | ||
req.jammin.document.owner = req.user._id; | ||
next(); | ||
} | ||
var ownersOnly = function(req, res, next) { | ||
req.jammin.query.owner = {"$eq": req.user.username}; | ||
req.jammin.query.owner = {"$eq": req.user._id}; | ||
next(); | ||
} | ||
API.Pets.get('/pets'); | ||
API.Pets.post('/pets', setOwnership); | ||
API.Pets.patch('/pets/:id', ownersOnly); | ||
API.Pets.delete('/pets/:id', ownersOnly); | ||
API.Pet.get('/pets'); | ||
API.Pet.post('/pets', setOwnership); | ||
API.Pet.patch('/pets/:id', ownersOnly); | ||
API.Pet.delete('/pets/:id', ownersOnly); | ||
``` | ||
You can also use middleware to alter calls to module functions. This function sanitizes calls to fs: | ||
### Manual Calls and Intercepting Results | ||
You can manually run a Jammin query and view the results before sending them to the user. Simply call the operation you want without a path. | ||
Jammin will automatically handle 404 and 500 errors, but will pass the results of successful operations to your callback. | ||
```js | ||
API.module('/files', {module: require('fs'), async: true}, function(req, res, next) { | ||
if (req.path.indexOf('Sync') !== -1) return res.status(400).send("Synchronous functions not allowed"); | ||
// Remove path traversals | ||
req.jammin.arguments[0] = Path.join('/', req.jammin.arguments[0]); | ||
// Make sure all operations are inside __dirname/user_files | ||
req.jammin.arguments[0] = Path.join(__dirname, 'user_files', req.jammin.arguments[0]); | ||
next(); | ||
}); | ||
app.get('/pets', J.limit(20), function(req, res) { | ||
API.Pet.getMany(req, res, function(pets) { | ||
res.json(pets); | ||
}) | ||
}) | ||
``` | ||
### Swagger (beta) | ||
Serve a [Swagger specification](http://swagger.io) for your API at the specified path. You can use this to document your API via [Swagger UI](https://github.com/swagger-api/swagger-ui) or a [LucyBot portal](https://lucybot.com) | ||
If you'd like to handle errors manually, you can also access the underlying model: | ||
```js | ||
API.swagger('/swagger.json'); | ||
API.Pet.model.findOneAndUpdate(...); | ||
``` | ||
Jammin will automatically fill out most of your spec, but you can provide additional information: | ||
```js | ||
var API = new Jammin.API({ | ||
databaseURL: DatabaseURL, | ||
swagger: { | ||
info: {title: 'Pet Store'}, | ||
host: 'api.example.com', | ||
basePath: '/api' | ||
} | ||
}); | ||
``` | ||
## Extended Usage | ||
See the example [Petstore Server](test/petstore-server.js) for other examples. |
var FS = require('fs'); | ||
var Hash = require('password-hash'); | ||
var Mongoose = require('mongoose'); | ||
require('mockgoose')(Mongoose); | ||
var App = require('express')(); | ||
App.use(require('cors')()); | ||
module.exports.listen = function(port) { | ||
module.exports.listen = function(port, done) { | ||
console.log('listening: ' + port); | ||
App.listen(port || 3000); | ||
Mongoose.connect('mongodb://example.com/TestingDB', done); | ||
} | ||
module.exports.dropAllEntries = function(callback) { | ||
API.Pet.db.remove({}, function(err) { | ||
if (err) throw err; | ||
API.User.db.remove({}, function(err) { | ||
if (err) throw err; | ||
callback(); | ||
}) | ||
}) | ||
} | ||
var DatabaseURL = JSON.parse(FS.readFileSync('./creds/mongo.json', 'utf8')).url; | ||
var Jammin = require('../index.js') | ||
var Jammin = require('../index.js'), | ||
J = Jammin.middleware; | ||
var API = new Jammin.API({ | ||
databaseURL: DatabaseURL, | ||
swagger: { | ||
info: {title: 'Pet Store', version: '0.1'}, | ||
host: 'api.example.com', | ||
basePath: '/api', | ||
securityDefinitions: { | ||
username: { name: 'username', in: 'header', type: 'string'}, | ||
password: { name: 'password', in: 'header', type: 'string'} | ||
}, | ||
definitions: { | ||
User: { | ||
properties: { | ||
username: {type: 'string'}, | ||
} | ||
}, | ||
} | ||
} | ||
connection: Mongoose, | ||
}); | ||
var UserSchema = { | ||
var UserSchema = Mongoose.Schema({ | ||
username: {type: String, required: true, unique: true, match: /^\w+$/}, | ||
password_hash: {type: String, required: true, select: false}, | ||
} | ||
}) | ||
@@ -54,3 +30,3 @@ var vaccSchema = Mongoose.Schema({ | ||
var PetSchema = { | ||
var PetSchema = Mongoose.Schema({ | ||
id: {type: Number, required: true, unique: true}, | ||
@@ -61,4 +37,7 @@ name: String, | ||
vaccinations: [vaccSchema] | ||
} | ||
}) | ||
API.addModel('Pet', Mongoose.model('Pet', PetSchema)); | ||
API.addModel('User', Mongoose.model('User', UserSchema)); | ||
var authenticateUser = function(req, res, next) { | ||
@@ -68,3 +47,3 @@ var query = { | ||
}; | ||
API.User.db.findOne(query).select('+password_hash').exec(function(err, user) { | ||
API.User.model.findOne(query).select('+password_hash').exec(function(err, user) { | ||
if (err) { | ||
@@ -83,26 +62,4 @@ res.status(500).json({error: err.toString()}) | ||
var SwaggerLogin = { | ||
swagger: { | ||
security: {username: [], password: []}, | ||
} | ||
} | ||
API.addModel('Pet', API.mongoose.model('Pet', new Mongoose.Schema(PetSchema))); | ||
API.addModel('User', API.mongoose.model('User', new Mongoose.Schema(UserSchema))); | ||
// Creates a new user. | ||
API.User.post('/users', { | ||
swagger: { | ||
parameters: [{ | ||
name: 'body', | ||
in: 'body', | ||
schema: { | ||
properties: { | ||
username: {type: 'string'}, | ||
password: {type: 'string'} | ||
} | ||
} | ||
}] | ||
} | ||
}, function(req, res, next) { | ||
API.User.post('/users', function(req, res, next) { | ||
req.jammin.document.password_hash = Hash.generate(req.body.password); | ||
@@ -122,10 +79,3 @@ next(); | ||
// Searches pets by name | ||
API.Pet.getMany('/search/pets', { | ||
swagger: { | ||
description: "Search all pets by name", | ||
parameters: [ | ||
{name: 'q', in: 'query', type: 'string', description: 'Any regex'} | ||
] | ||
} | ||
}, function(req, res, next) { | ||
API.Pet.getMany('/search/pets', J.sort('+name'), function(req, res, next) { | ||
req.jammin.query = { | ||
@@ -142,3 +92,3 @@ name: { "$regex": new RegExp(req.query.q) } | ||
API.Pet.post('/pets/:id', SwaggerLogin, upsert, authenticateUser, function(req, res, next) { | ||
API.Pet.post('/pets/:id', upsert, authenticateUser, function(req, res, next) { | ||
req.jammin.document.owner = req.user.username; | ||
@@ -150,3 +100,3 @@ req.jammin.document.id = req.params.id; | ||
// Creates one or more new pets. | ||
API.Pet.postMany('/pets', SwaggerLogin, authenticateUser, function(req, res, next) { | ||
API.Pet.postMany('/pets', authenticateUser, function(req, res, next) { | ||
if (!Array.isArray(req.jammin.document)) req.jammin.document = [req.jammin.document]; | ||
@@ -167,12 +117,12 @@ req.jammin.document.forEach(function(pet) { | ||
// Changes a pet. | ||
API.Pet.patch('/pets/:id', SwaggerLogin, authenticateUser, enforceOwnership); | ||
API.Pet.patch('/pets/:id', authenticateUser, enforceOwnership); | ||
// Changes every pet that matches the query. | ||
API.Pet.patchMany('/pets', SwaggerLogin, authenticateUser, enforceOwnership); | ||
API.Pet.patchMany('/pets', authenticateUser, enforceOwnership); | ||
// Deletes a pet by ID. | ||
API.Pet.delete('/pets/:id', SwaggerLogin, authenticateUser, enforceOwnership); | ||
API.Pet.delete('/pets/:id', authenticateUser, enforceOwnership); | ||
// Deletes every pet that matches the query. | ||
API.Pet.deleteMany('/pets', SwaggerLogin, authenticateUser, enforceOwnership); | ||
API.Pet.deleteMany('/pets', authenticateUser, enforceOwnership); | ||
@@ -187,4 +137,2 @@ API.router.get('/pet_count', function(req, res) { | ||
API.swagger('/swagger.json'); | ||
App.use('/api', API.router); |
@@ -7,3 +7,2 @@ var _ = require('lodash'); | ||
var SWAGGER_GOLDEN_FILE = __dirname + '/golden/petstore.swagger.json'; | ||
var BASE_URL = 'http://127.0.0.1:3333/api'; | ||
@@ -68,4 +67,3 @@ | ||
before(function(done) { | ||
Petstore.listen(3333); | ||
Petstore.dropAllEntries(done); | ||
Petstore.listen(3333, done); | ||
}); | ||
@@ -336,19 +334,2 @@ | ||
}) | ||
it('should serve swagger docs', function(done) { | ||
Request.get({ | ||
url: BASE_URL + '/swagger.json', | ||
json: true | ||
}, function(err, res, body) { | ||
Expect(err).to.equal(null); | ||
if (process.env.WRITE_GOLDEN) { | ||
console.log("Writing new golden file!"); | ||
FS.writeFileSync(SWAGGER_GOLDEN_FILE, JSON.stringify(body, null, 2)); | ||
} else { | ||
var golden = JSON.parse(FS.readFileSync(SWAGGER_GOLDEN_FILE, 'utf8')); | ||
Expect(body).to.deep.equal(golden); | ||
} | ||
done(); | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
20
1
4
52443
5
1130
181