Socket
Socket
Sign inDemoInstall

resource-schema

Package Overview
Dependencies
Maintainers
1
Versions
53
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

resource-schema - npm Package Compare versions

Comparing version 0.7.0 to 0.8.0

lib/deep_extend.js

384

lib/index.js
// Generated by CoffeeScript 1.8.0
var RESERVED_KEYWORDS, ResourceSchema, dot, q, _,
var RESERVED_KEYWORDS, ResourceSchema, clone, deepExtend, dot, q, _,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },

@@ -12,5 +12,9 @@ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

RESERVED_KEYWORDS = ['$find', '$get', '$set', '$field', '$optional'];
clone = require('clone');
deepExtend = require('./deep_extend');
RESERVED_KEYWORDS = ['$find', '$get', '$set', '$field', '$optional', '$validate', '$match', '$type', '$isArray'];
/*

@@ -24,5 +28,7 @@ normalized schema:

dynamicField: {
$find: ->
$get: ->
$set: ->
$validate: (value) ->
match: ->
$find: (value, done) ->
$get: (resources, request, done) ->
$set: (models, request, done) ->
}

@@ -36,8 +42,8 @@ }

this.options = options != null ? options : {};
this._normalizeQueryParams = __bind(this._normalizeQueryParams, this);
this._normalizeSchema = __bind(this._normalizeSchema, this);
this._getSchemaFromModel = __bind(this._getSchemaFromModel, this);
this._generateSchemaFromModel = __bind(this._generateSchemaFromModel, this);
this._getResourceAndModelFields = __bind(this._getResourceAndModelFields, this);
this._convertKeysToDotStrings = __bind(this._convertKeysToDotStrings, this);
this._selectValidResourceSearchFields = __bind(this._selectValidResourceSearchFields, this);
this._selectValidQuerySearchFields = __bind(this._selectValidQuerySearchFields, this);
this._getModelSelectFields = __bind(this._getModelSelectFields, this);

@@ -48,7 +54,7 @@ this._getAddFields = __bind(this._getAddFields, this);

this._getGroupQuery = __bind(this._getGroupQuery, this);
this._resolveResourceGetPromises = __bind(this._resolveResourceGetPromises, this);
this._resolveResourceSetPromises = __bind(this._resolveResourceSetPromises, this);
this._applyGetters = __bind(this._applyGetters, this);
this._applySetters = __bind(this._applySetters, this);
this._createResourceFromModel = __bind(this._createResourceFromModel, this);
this._createModelFromResource = __bind(this._createModelFromResource, this);
this._getQueryConfigPromise = __bind(this._getQueryConfigPromise, this);
this._getMongoQuery = __bind(this._getMongoQuery, this);
this.send = __bind(this.send, this);

@@ -60,3 +66,3 @@ this._getOne = __bind(this._getOne, this);

} else {
this.schema = this._getSchemaFromModel(this.Model);
this.schema = this._generateSchemaFromModel(this.Model);
}

@@ -80,2 +86,5 @@ }

var limit, modelSelect, sendResources;
if (!this._isValid(req.query, res)) {
return;
}
sendResources = (function(_this) {

@@ -87,3 +96,6 @@ return function(err, modelsFound) {

});
return _this._resolveResourceGetPromises(resources, modelsFound, req.query).then(function() {
return _this._applyGetters(resources, modelsFound, {
req: req,
res: res
}).then(function() {
res.body = resources;

@@ -96,12 +108,15 @@ return next();

modelSelect = this._getModelSelectFields(req.query);
return this._getQueryConfigPromise(req.query).then((function(_this) {
return function(queryConfig) {
return this._getMongoQuery(req.query, {
req: req,
res: res
}).then((function(_this) {
return function(mongoQuery) {
var modelQuery;
if (_this.options.groupBy) {
modelQuery = _this.Model.aggregate();
modelQuery.match(queryConfig);
modelQuery.match(mongoQuery);
modelQuery.group(_this._getGroupQuery());
}
if (!_this.options.groupBy) {
modelQuery = _this.Model.find(queryConfig);
modelQuery = _this.Model.find(mongoQuery);
modelQuery.select(modelSelect);

@@ -122,2 +137,5 @@ modelQuery.lean();

var idValue, modelQuery, query, select;
if (!_this._isValid(req.query, res)) {
return;
}
select = _this._getModelSelectFields(req.query);

@@ -141,3 +159,6 @@ idValue = req.params[paramId];

resource = _this._createResourceFromModel(modelFound, req.query.$select);
return _this._resolveResourceGetPromises([resource], [modelFound], req.query).then(function() {
return _this._applyGetters([resource], [modelFound], {
req: req,
res: res
}).then(function() {
res.body = resource;

@@ -159,14 +180,27 @@ return next();

return function(req, res, next) {
var model, newModelData;
newModelData = _this._createModelFromResource(req.body);
model = new _this.Model(newModelData);
return model.save(function(err, modelSaved) {
var resource;
if (err) {
res.send(400, err);
}
resource = _this._createResourceFromModel(modelSaved, req.query.$select);
res.status(201);
res.body = resource;
return next();
var newModelData, resource;
if (!_this._isValid(req.query, res)) {
return;
}
resource = req.body;
if (!_this._isValid(resource, res)) {
return;
}
_this._convertTypes(resource, res);
newModelData = _this._createModelFromResource(resource);
return _this._applySetters([resource], [newModelData], {
req: req,
res: res
}).then(function() {
var model;
model = new _this.Model(newModelData);
return model.save(function(err, modelSaved) {
if (err) {
return res.status(400).send(err);
}
resource = _this._createResourceFromModel(modelSaved, req.query.$select);
res.status(201);
res.body = resource;
return next();
});
});

@@ -186,2 +220,8 @@ };

var idValue, newModelData, query;
if (!_this._isValid(req.query, res)) {
return;
}
if (!_this._isValid(req.body, res)) {
return;
}
newModelData = _this._createModelFromResource(req.body);

@@ -191,19 +231,20 @@ idValue = req.params[paramId];

query[paramId] = idValue;
return _this.Model.findOne(query).lean().exec(function(err, modelFound) {
return _this._resolveResourceSetPromises(req.body, modelFound, {}).then(function() {
return _this.Model.findOneAndUpdate(query, newModelData, {
upsert: true
}).lean().exec(function(err, modelUpdated) {
var resource;
if (err) {
res.send(400, err);
}
if (!modelUpdated) {
res.send(404, 'resource not found');
}
resource = _this._createResourceFromModel(modelUpdated, req.query.$select);
res.status(200);
res.body = resource;
return next();
});
return _this._applySetters([req.body], [newModelData], {
req: req,
res: res
}).then(function() {
return _this.Model.findOneAndUpdate(query, newModelData, {
upsert: true
}).lean().exec(function(err, modelUpdated) {
var resource;
if (err) {
return res.send(400, err);
}
if (!modelUpdated) {
return res.send(404, 'resource not found');
}
resource = _this._createResourceFromModel(modelUpdated, req.query.$select);
res.status(200);
res.body = resource;
return next();
});

@@ -224,2 +265,5 @@ });

var idValue, query;
if (!_this._isValid(req.query, res)) {
return;
}
idValue = req.params[paramId];

@@ -230,6 +274,6 @@ query = {};

if (err) {
res.send(400, err);
return res.status(400).send(err);
}
if (removedInstance == null) {
res.send(404, "Resource with id " + idValue + " not found from " + _this.Model.modelName + " collection");
res.status(404).send("Resource with id " + idValue + " not found from " + _this.Model.modelName + " collection");
}

@@ -261,9 +305,10 @@ res.status(204);

ResourceSchema.prototype._getQueryConfigPromise = function(requestQuery) {
var d, deferred, modelQuery, queryField, queryPromises, querySearchFields, resourceField, resourceSearchFields, value;
modelQuery = _.clone(this.options.defaultQuery) || {};
ResourceSchema.prototype._getMongoQuery = function(requestQuery, _arg) {
var d, deferred, modelQuery, queryPromises, req, res, resourceField, resourceSearchFields, value;
req = _arg.req, res = _arg.res;
modelQuery = clone(this.options.defaultQuery) || {};
deferred = q.defer();
queryPromises = [];
resourceSearchFields = this._selectValidResourceSearchFields(requestQuery);
querySearchFields = this._selectValidQuerySearchFields(requestQuery);
this._convertTypes(resourceSearchFields);
if (resourceSearchFields) {

@@ -274,6 +319,11 @@ for (resourceField in resourceSearchFields) {

d = q.defer();
this.schema[resourceField].$find(value, function(err, query) {
_(modelQuery).extend(query);
return d.resolve();
});
this.schema[resourceField].$find(value, {
req: req,
res: res
}, (function(_this) {
return function(err, query) {
deepExtend(modelQuery, query);
return d.resolve();
};
})(this));
queryPromises.push(d.promise);

@@ -285,13 +335,2 @@ } else if (this.schema[resourceField].$field) {

}
if (querySearchFields) {
for (queryField in querySearchFields) {
value = querySearchFields[queryField];
d = q.defer();
this.options.queryParams[queryField](value, function(err, query) {
_(modelQuery).extend(query);
return d.resolve();
});
queryPromises.push(d.promise);
}
}
q.all(queryPromises).then(function() {

@@ -354,4 +393,5 @@ return deferred.resolve(modelQuery);

ResourceSchema.prototype._resolveResourceSetPromises = function(resource, model, queryParams) {
var config, d, resourceField, setPromises, _ref;
ResourceSchema.prototype._applySetters = function(resources, models, _arg) {
var config, d, req, res, resourceField, setPromises, _ref;
req = _arg.req, res = _arg.res;
setPromises = [];

@@ -363,3 +403,7 @@ _ref = this.schema;

d = q.defer();
config.$set(resource[resourceField], model, queryParams, function(err, results) {
config.$set(models, {
req: req,
res: res,
resources: resources
}, function(err, results) {
return d.resolve();

@@ -378,6 +422,7 @@ });

ResourceSchema.prototype._resolveResourceGetPromises = function(resources, models, query) {
var config, getPromises, resourceField, resourceSelectFields, _ref;
ResourceSchema.prototype._applyGetters = function(resources, models, _arg) {
var config, getPromises, req, res, resourceField, resourceSelectFields, _ref;
req = _arg.req, res = _arg.res;
getPromises = [];
resourceSelectFields = this._getResourceSelectFields(query);
resourceSelectFields = this._getResourceSelectFields(req.query);
_ref = this.schema;

@@ -390,5 +435,9 @@ for (resourceField in _ref) {

d = q.defer();
config.$get(resources, models, query, function(err, results) {
config.$get(resources, {
req: req,
res: res,
models: models
}, function(err, results) {
if (err) {
console.log(err);
throw new Error(err);
}

@@ -440,3 +489,3 @@ return d.resolve();

ResourceSchema.prototype._getLimit = function(query) {
return query.$limit || this.options.defaultLimit || 100;
return query.$limit || this.options.defaultLimit;
};

@@ -464,3 +513,4 @@

/*
Get all valid $add fields from the query. Used to select $optional fields from schema
Get all valid $add fields from the query. The add fields are used to
select $optional fields from schema
@param [Object] query - query params from client

@@ -504,26 +554,2 @@ @returns [Array] valid keys to add from schema

/*
Select valid properties from query that can be used for filtering resources
@param [Object] query - query params from client
@returns [Object] valid query fields and their values
*/
ResourceSchema.prototype._selectValidQuerySearchFields = function(query) {
var field, queryDotString, queryParamFields, validFields, value;
if (!this.options.queryParams) {
return {};
}
queryDotString = this._convertKeysToDotStrings(query);
queryParamFields = Object.keys(this.options.queryParams);
validFields = {};
for (field in queryDotString) {
value = queryDotString[field];
if (__indexOf.call(queryParamFields, field) >= 0) {
dot.set(validFields, field, value);
}
}
return this._convertKeysToDotStrings(validFields);
};
/*
Select valid properties from query that can be used for filtering resources in the schema

@@ -550,3 +576,4 @@ @param [Object] query - query params from client

/*
Collapse all nested fields dot format
Collapse all nested fields to dot format. Ignore Reserved Keywords.
This is used for the schema, the query params, and the incoming resources
@example {a: {b: 1}} -> {'a.b': 1}

@@ -594,3 +621,3 @@ */

ResourceSchema.prototype._getSchemaFromModel = function(Model) {
ResourceSchema.prototype._generateSchemaFromModel = function(Model) {
var schema, schemaKey, schemaKeys, _i, _len;

@@ -646,7 +673,158 @@ schemaKeys = Object.keys(Model.schema.paths);

}
_(normalizedSchema).extend(this._normalizeQueryParams());
return normalizedSchema;
};
ResourceSchema.prototype._normalizeQueryParams = function() {
var config, normalizedParams, param, _ref;
normalizedParams = {};
if (this.options.queryParams) {
_ref = this.options.queryParams;
for (param in _ref) {
config = _ref[param];
if (typeof config === 'function') {
normalizedParams[param] = {
$find: config
};
} else if (typeof config === 'object') {
normalizedParams[param] = config;
} else {
throw new Error("QueryParam config for " + param + " must be either a configuration object or a function");
}
}
}
return normalizedParams;
};
/*
Check validity of object with $validate and $match on schema
*/
ResourceSchema.prototype._isValid = function(obj, res) {
var key, normalizedObj, v, validateValue, value, _i, _len;
validateValue = (function(_this) {
return function(key, value, res) {
var _ref, _ref1;
if ((_ref = _this.schema[key]) != null ? _ref.$validate : void 0) {
if (!_this.schema[key].$validate(value)) {
res.status(400).send("'" + key + "' is invalid");
return false;
}
}
if ((_ref1 = _this.schema[key]) != null ? _ref1.$match : void 0) {
if (!_this.schema[key].$match.test(value)) {
res.status(400).send("'" + key + "' is invalid");
return false;
}
}
return true;
};
})(this);
normalizedObj = this._convertKeysToDotStrings(obj);
for (key in normalizedObj) {
value = normalizedObj[key];
if (Array.isArray(value)) {
for (_i = 0, _len = value.length; _i < _len; _i++) {
v = value[_i];
if (!validateValue(key, v, res)) {
return false;
}
}
} else {
if (!validateValue(key, value, res)) {
return false;
}
}
}
return true;
};
/*
By default, all query parameters are sent as strings.
This method converts those strings to the appropriate types for data manipulation
Supports:
- String
- Date
- Number
- Boolean
- mongoose.Types.ObjectId and other newable objects
TODO: needs to be tested
*/
ResourceSchema.prototype._convertTypes = function(obj, res) {
var convert, i, key, send400, v, value, _ref, _ref1, _results;
send400 = (function(_this) {
return function(type, key, value) {
return res.status(400).send("'" + value + "' is an invalid Date for field '" + key + "'");
};
})(this);
convert = (function(_this) {
return function(key, value) {
var date, e, newValue, number;
switch (_this.schema[key].$type) {
case String:
return value;
case Number:
number = parseFloat(value);
if (isNaN(number)) {
send400('Number', key, value);
}
return number;
case Boolean:
if ((value === 'true') || (value === true)) {
return true;
} else if ((value === 'false') || (value === true)) {
return false;
} else {
return send400('Boolean', key, value);
}
break;
case Date:
date = new Date(value);
if (isNaN(date.getTime())) {
send400('Date', key, value);
}
return date;
default:
try {
newValue = new _this.schema[key].$type(value);
return newValue;
} catch (_error) {
e = _error;
return res.status(400).send(e);
}
}
};
})(this);
_results = [];
for (key in obj) {
value = obj[key];
if (((_ref = this.schema[key]) != null ? _ref.$type : void 0) == null) {
continue;
}
if ((_ref1 = this.schema[key]) != null ? _ref1.$isArray : void 0) {
if (!Array.isArray(value)) {
obj[key] = [value];
}
_results.push((function() {
var _ref2, _results1;
_ref2 = obj[key];
_results1 = [];
for (i in _ref2) {
v = _ref2[i];
_results1.push(obj[key][i] = convert(key, v));
}
return _results1;
})());
} else {
_results.push(obj[key] = convert(key, value));
}
}
return _results;
};
return ResourceSchema;
})();
{
"name": "resource-schema",
"version": "0.7.0",
"version": "0.8.0",
"description": "Define schemas for RESTful resources from mongoose models, and generate middleware to GET, POST, PUT, and DELETE to those resources.",

@@ -21,3 +21,5 @@ "author": "Good Eggs <open-source@goodeggs.com>",

"q": "^1.0.1",
"async": "^0.9.0"
"async": "^0.9.0",
"deep-extend": "^0.3.2",
"clone": "^0.1.18"
},

@@ -24,0 +26,0 @@ "devDependencies": {

@@ -8,2 +8,306 @@ # Resource Schema

## Why ResourceSchema?
ResourceSchema allows you to define complex RESTful resources in a simple and declarative way. For example:
```coffeescript
schema = {
'_id'
'name'
'isActive': 'active'
'totalQuantitySold':
$optional: true
$get: addTotalQuantitySold # method defined elsewhere
}
queryParams =
'soldOn':
$type: String
$isArray: true
$match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
$find: fibrous (days) -> { 'day': $in: days }
'fromLastWeek':
$type: Boolean
$find: fibrous (days) -> { 'day': $gt: '2014-10-12' }
resource = ResourceSchema(Product, schema, {queryParams})
app.get '/', resource.get(), resource.send
app.post '/', resource.post(), resource.send
app.put '/:_id', resource.put('_id'), resource.send
app.get '/:_id', resource.get('_id'), resource.send
app.delete '/:_id', resource.delete('_id'), resource.send
```
This abstracts away a lot of the boilerplate such as error handling or validating query parameters, and allows you to focus on higher-level resource design.
## Generating Middleware
Once you have defined a new resource, call get, post, put, or delete to generate the appropriate middleware to handle the request.
``` coffeescript
resource = new ResourceSchema(Model, schema, options)
app.get '/products', resource.get(), (req, res, next) ->
# resources are on res.body
```
the middleware will attach the resources to res.body, which can then be used by other pieces of middleware, or sent immediately back to the client
### resource.get()
Generate middleware to handle GET requests for multiple resources.
### resource.post()
Generate middleware to handle POST requests to a resource.
### resource.put()
Generate middleware to handle PUT requests to a resource.
### resource.delete()
Generate middleware to handle DELETE requests to a resource.
### resource.send
Convenience method for sending the resource back to the client.
``` coffeescript
resource = new ResourceSchema(Model, schema, options)
app.get '/products', resource.get(), resource.send
```
## Defining a schema
### $field
Maps a mongoose model field to a resource field.
``` javascript
schema = {
'name': { $field: 'name' }
}
```
We can also define this with a shorthand notation:
``` javascript
schema = {
'name': 'name'
}
```
Or even simpler with coffeescript:
``` javascript
schema = {
'name'
}
```
Note, this can be used to rename a model field to a new name on the resource:
``` javascript
schema = {
'category.name': 'categoryName'
}
// {
// category: {
// name: 'value'
// }
// }
```
### $get
Dynamically get the value whenever a resource is retrieved.
``` javascript
schema = {
'totalProductsSold': {
$get: (resourcesToReturn, {models, req, res, next}, done) ->
resourcesToReturn.forEach (resource) ->
resource.totalProductsSold = 10
done()
}
}
```
### $set
Dynamically set the value whenever a resource is saved or updated
``` javascript
schema = {
'name': {
$set: (modelsToSave, {resources, req, res, next}, done) ->
modelsToSave.forEach (model) ->
model.name = model.name.toLowerCase()
done()
}
}
```
### $find
Dynamically find resources with the provided query value. Return an object that will extend the mongoose query. $find is used to define query parameters.
``` javascript
schema = {
'soldOn':
$find: fibrous (days, {req, res, next}) ->
{ 'day': $in: days }
}
```
### $optional
If true, do not include the value in the resource unless specifically requested by the client with the '$add' query parameter
``` coffeescript
// GET /api/products?$add=name
schema = {
'name': {
$optional: true
$field: 'name'
}
}
```
### $validate
Return a 400 invalid request if the provided value does not pass the validation test.
``` coffeescript
schema = {
'date': {
$validate: (value) ->
/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/.test(value)
}
}
```
### $match
Return a 400 invalid request if the provided value does match the given regular expression.
``` coffeescript
schema = {
'date': {
$match:/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
}
}
```
### $type
Convert the type of the value.
Valid types include
- String
- Date
- Number
- Boolean
- and any other "newable" class
``` coffeescript
schema = {
'active': {
$type: Boolean
}
}
```
This is especially valuable for query parameters, since they are all a string by default.
### $isArray
Ensure that the query parameter is an Array.
``` coffeescript
schema = {
'daysToSelect': {
$isArray: Boolean
$find: (days, context, done) -> ...
}
}
```
## options
### options.defaultLimit
Set the default number of the resources that will be sent for GET requests.
``` coffeescript
new ResourceSchema(Product, schema, {
defaultLimit: 100
})
```
### options.defaultQuery
Set the default query for this resource. All other query parameters will extend this query.
``` coffeescript
new ResourceSchema(Product, schema, {
defaultQuery: {
active: true
createdAt: $gt: '2013-01-01'
}
})
```
### options.queryParams
Define query parameters for this resource using the $find method. Note that these query parameters can be defined directly on the schema, but you can define them here if you prefer (since query parameters are often not part of the resource being returned).
```coffeescript
queryParams =
'soldOn':
$type: String
$isArray: true
$match: /[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
$find: fibrous (days) -> { 'day': $in: days }
'fromLastWeek':
$type: Boolean
$find: fibrous (days) -> { 'day': $gt: '2014-10-12' }
```
## Querying the resources
ResourceSchema automatically adds several utilities for interacting with your resources.
### Querying by resource field
Query by any resource field with a $field or a $find attribute.
```
GET /products?name=strawberry
GET /products?categrory[name]=fruit
```
Note that you can query nested fields with Express' [bracket] notation.
### $select
Select fields to return on the resource. Similar to mongoose select.
```
GET /products?$select=name&$select=active
GET /products?$select[]=name&$select[]=active
GET /products?$select=name%20active
```
### $limit
Limit the number of resources to return in the response
```
GET /products?$limit=10
```
### $add
Add an $optional field to the response. See $optional schema fields for more details.
```
GET /products?$add=quantitySold
```
## Contributing

@@ -10,0 +314,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc