Coder Social home page Coder Social logo

epilogue's Introduction

Build Status Dependency Status

Epilogue

Create flexible REST endpoints and controllers from Sequelize models in your Express or Restify app.

Getting Started

var Sequelize = require('sequelize'),
    epilogue = require('epilogue'),
    http = require('http');

// Define your models
var database = new Sequelize('database', 'root', 'password');
var User = database.define('User', {
  username: Sequelize.STRING,
  birthday: Sequelize.DATE
});

// Initialize server
var server, app;
if (process.env.USE_RESTIFY) {
  var restify = require('restify');

  app = server = restify.createServer()
  app.use(restify.queryParser());
  app.use(restify.bodyParser());
} else {
  var express = require('express'),
      bodyParser = require('body-parser');

  var app = express();
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));
  server = http.createServer(app);
}

// Initialize epilogue
epilogue.initialize({
  app: app,
  sequelize: database
});

// Create REST resource
var userResource = epilogue.resource({
  model: User,
  endpoints: ['/users', '/users/:id']
});

// Create database and listen
database
  .sync({ force: true })
  .then(function() {
    server.listen(function() {
      var host = server.address().address,
          port = server.address().port;

      console.log('listening at http://%s:%s', host, port);
    });
  });

Controllers and endpoints

On the server we now have the following controllers and endpoints:

Controller Endpoint Description
userResource.create POST /users Create a user
userResource.list GET /users Get a listing of users
userResource.read GET /users/:id Get details about a user
userResource.update PUT /users/:id Update a user
userResource.delete DELETE /users/:id Delete a user

Customize behavior

Of course it's likely that we'll want more flexibility. Our users resource has properties for each of the controller actions. Controller actions in turn have hooks for setting and overriding behavior at each step of the request. We have these milestones to work with: start, auth, fetch, data, write, send, and complete.

var ForbiddenError = require('epilogue').Errors.ForbiddenError;

// disallow deletes on users
userResource.delete.auth(function(req, res, context) {
    throw new ForbiddenError("can't delete a user");
    // optionally:
    // return context.error(403, "can't delete a user");
})

We can set behavior for milestones directly as above, or we can add functionality before and after milestones too:

// check the cache first
userResource.list.fetch.before(function(req, res, context) {
	var instance = cache.get(context.criteria);

	if (instance) {
		// keep a reference to the instance and skip the fetch
		context.instance = instance;
		return context.skip;
	} else {
		// cache miss; we continue on
		return context.continue;
	}
})

Milestones can also be defined in a declarative fashion, and used as middleware with any resource. For example:

// my-middleware.js
module.exports = {
  create: {
    fetch: function(req, res, context) {
      // manipulate the fetch call
      return context.continue;
    }
  },
  list: {
    write: {
      before: function(req, res, context) {
        // modify data before writing list data
        return context.continue;
      },
      action: function(req, res, context) {
        // change behavior of actually writing the data
        return context.continue;
      },
      after: function(req, res, context) {
        // set some sort of flag after writing list data
        return context.continue;
      }
    }
  }
};

// my-app.js
var epilogue = require('epilogue'),
    restMiddleware = require('my-middleware');

epilogue.initialize({
    app: app,
    sequelize: sequelize
});

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id']
});

userResource.use(restMiddleware);

Epilogue middleware also supports bundling in extra resource configuration by specifying an "extraConfiguration" member of the middleware like so:

// my-middleware.js
module.exports = {
  extraConfiguration: function(resource) {
    // support delete for plural form of a resource
    var app = resource.app;
    app.del(resource.endpoints.plural, function(req, res) {
      resource.controllers.delete._control(req, res);
    });
  }
};

To show an error and halt execution of milestone functions you can throw an error:

var ForbiddenError = require('epilogue').Errors.ForbiddenError;

before: function(req, res, context) {
    return authenticate.then(function(authed) {
        if(!authed) throw new ForbiddenError();

        return context.continue;
    });
}

REST API

Listing resources support filtering, searching, sorting, and pagination as described below.

Filtering

Add query parameters named after fields to limit results.

$ curl http://localhost/users?name=James+Conrad

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
  }
]

Filtering using scope

Use scope to add additional filtering (More about scopes in sequelize - http://docs.sequelizejs.com/en/latest/docs/scopes/).

  // Define scope in model
  ...
  scope: {
    verified: {
      where : {
        email_verified: true
        phone_verified: true
      }  
    }
  }
$ curl http://localhost/users?scope=verified

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
    "email_verified": true,
    "phone_verified": true
  }
]

Search

Use the q parameter to perform a substring search across all fields.

$ curl http://localhost/users?q=james

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
  }, {
    "name": "Jim Huntington",
    "email": "[email protected]"
  }
]

Search behavior can be customized to change the parameter used for searching, as well as which attributes are included in the search, like so:

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: {
      param: 'searchOnlyUsernames',
      attributes: [ 'username' ]
    }
});

This would restrict substring searches to the username attribute of the User model, and the search parameter would be 'searchOnlyUsernames':

$ curl http://localhost/users?searchOnlyUsernames=james

By default, the substring search is performed using a {field} LIKE '%{query}%' pattern. However, this behavior can be customized by specifying a search operator. Valid operators include: $like (default), $ilike/$iLike, $notLike, $notILike, $ne, $eq, $not, $gte, $gt, $lte, $lt. All "*like" operators can only be used against Sequelize.STRING or Sequelize.TEXT fields. For instance:

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: {
      operator: '$gt',
      attributes: [ 'age' ]
    }
});

When querying against a Sequelize.BOOLEAN field, you'll need to use the $eq operator. You can also add multiple search parameters by passing the search key an array of objects:

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: [
      {operator: '$eq', param: 'emailVerified', attributes: [ 'email_verified' ]},
      {param: 'searchOnlyUsernames', attributes: [ 'username' ]}
    ] 
});

Sorting

Specify the sort parameter to sort results. Values are field names, optionally preceded by a - to indicate descending order. Multiple sort values may be separated by ,.

$ curl http://localhost/users?sort=-name

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "Jim Huntington",
    "email": "[email protected]"
  }, {
    "name": "James Conrad",
    "email": "[email protected]"
  }
]

Sort behavior can be customized to change the parameter used for sorting, as well as which attributes are allowed to be used for sorting like so:

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    sort: {
      param: 'orderby',
      attributes: [ 'username' ]
    }
});

This would restrict sorting to only the username attribute of the User model, and the sort parameter would be 'orderby':

$ curl http://localhost/users?orderby=username

Default sort criteria can be defined with the default attribute. The expected format for default sort criteria is exactly the same as if it was proceeding the sort parameter in the URL.

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    sort: {
      default: '-email,username'
    }
});

With this configuration, these two calls would result in the same data:

$ curl http://localhost/users
$ curl http://localhost/users?sort=-email,username

Note that the sort parameter in the URL will override your default criteria.

By default all attributes defined on the model are allowed to be sorted on. Sorting on a attribute not allowed will cause a 400 error to be returned with errors in the format:

$ curl http://localhost/users?sortby=invalid,-otherinvalid,valid

HTTP/1.1 400 BAD REQUEST
Content-Type: application/json

{
  "message": "Sorting not allowed on given attributes",
  "errors": ["invalid", "otherinvalid"]
}

Pagination

List routes support pagination via offset or page and count query parameters. Find metadata about pagination and number of results in the Content-Range response header. Pagination defaults to a default of 100 results per page, and a maximum of 1000 results per page.

# get the third page of results
$ curl http://localhost/users?offset=200&count=100

HTTP/1.1 200 OK
Content-Type: application/json
Content-Range: items 200-299/3230

[
  { "name": "James Conrad", ... },
  ...
]

Alternatively, you can specify that pagination is disabled for a given resource by passing false to the pagination property like so:

var userResource = epilogue.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    pagination: false
});

Epilogue API

initialize()

Set defaults and give epilouge a reference to your express app. Send the following parameters:

app

A reference to the Express application

base

Prefix to prepend to resource endpoints

updateMethod

HTTP method to use for update routes, one of POST, PUT, or PATCH

resource()

Create a resource and CRUD actions given a Sequelize model and endpoints. Accepts these parameters:

model

Reference to a Sequelize model

endpoints

Specify endpoints as an array with two sinatra-style URL paths in plural and singular form (e.g., ['/users', '/users/:id']).

actions

Create only the specified list of actions for the resource. Options include create, list, read, update, and delete. Defaults to all.

excludeAttributes

Explicitly remove the specified list of attributes from read and list operations

Milestones & Context

Check out the Milestone docs for information on lifecycle hooks that can be used with epilogue resources, and how to run custom code at various points during a request.

Protecting Epilogue REST Endpoints

To protect an endpoint, you must use milestones.

In order to protect and endpoint (for example, to require that only a logged in user or user with the appropriate security token can access a resource) you need to use the appropriate milestone hooks.

Below is an example of how to do this with standard Express middleware, which is commonly used to protect resources. Note that the callback functions required by Epilogue milestones look similar to express middleware, but the third argument (context) is different.

Suppose you have this resource:

var userResource = rest.resource({
    model: User
});

To protect all endpoints, we'll use userResource.all.auth, a hook used to authorize the endpoint before any operation (create, list, etc). Suppose also we have an express middlware function called authorize(req, res, done). This authorize function might for example be a passport strategy such as passport('local').

To authorize the endpoint, you would do this:

userResource.all.auth(function (req, res, context) {
  return new Promise(function(resolve, reject) {
    authorize(req, res, function (arg) {
      if (arg) {
        // Middleware function returned an error; this means the operation
        // should not be authorized.
        res.status(401).send({message: "Unauthorized"});
        resolve(context.stop);
      } else {
        resolve(context.continue);
      }
  });
})

In this code, note that userResource.all.auth is simply reusing the express middleware to do whatever authorization checking your code requires. We are passing a custom done function to the middleware, which resolves a promise as either context.stop or context.continue, indicating to epilogue whether or not to proceed. Note that in the case where the transaction isn't authorized, epilogue won't proceed, so it is your responsibility to send a response back to the client.

Further Information on Protecting Endpoints

The milestone documentation provides many other hooks for finer-grained operations, i.e. permitting all users to list but only some users to delete can be implemented by using the same approach described above, with different milestones.

License

Copyright (C) 2012-2015 David Chester Copyright (C) 2014-2015 Matt Broadstone

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

epilogue's People

Contributors

ackerdev avatar adampflug avatar asmodehn avatar bartolomeon avatar bighitbiker3 avatar chaunax avatar chinayuans avatar dchester avatar eluinhost avatar felipemsantana avatar fridus avatar h256 avatar jblaketc avatar johngiudici avatar karolisg avatar lennym avatar loicmahieu avatar mbroadst avatar moxious avatar mykola-den avatar omayhemo avatar petekeller2 avatar philotas avatar prayagverma avatar satyajeetcmm avatar treefort avatar wilzbach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

epilogue's Issues

Does the REST api handle multiple conditions and inequalities

I want to fetch all the stops within a given rectangle (the current view of the map)
Can I cook this up with a URL on the existing framework or do I need to hatch my own API call.
I saw the search/operator doco which is obviously what I need if I need to make this, I was just checking that it's up to me to make a special resource.
It would be nice to be able to go something like

/Stops?lat=$gt$:min_lat&lat=$lt$:max_lat&lon=$lt$:min_lon&lon=$lt$:max_lon

How can we get data with search criteria?

Hi;

Thanks for the wonderful library. But Readme could be better.

I have a question. How can we get an object list with a search criteria?

E.g: /items?categoryId=1

Thank you.

Always return 'respond with a resource'

I am using express generator. I put this code in bin/www. Here my code snapshot..

var app = require('../app'); // return app = express();
var models = require("../models"); // return sequelize initiation
var epilogue = require('epilogue');

...

epilogue.initialize({
  sequelize: models.Sequelize
});

var userResource = epilogue.resource({
  model: models.User,
  endpoints: ['/users', '/users/:id']
});

...

When I run the app. No error showing, but when I open http://localhost:3000/users it always return

respond with a resource

What I miss here. Or Did I make a mistake.
Where should I put the initiation of epilogue to make it works.

Foreign key being deleted from associated fetch

/Routes/1/Trips drops the RouteId, which is 1 of course so it's not a show stopper for me to have to put it back. Is there is a way of asking for it not to be removed ?,

I thought I had the block of code that was doing that at the end of list.js but I remarked it out and RouteId was still missing.

pagination off not working

pagination has got mixed up, it's always true once we get to List.prototype.fetch

 if (!this.resource.pagination)
    delete options.limit;

Ah, further investigation shows that we are calling Resource on these related models, and it's flipping pagination back to the default of true for them. As everything is related to something, it always gets Resource called on it second hand and pagination turned back on.
I dont need pagination, though I will, though may be > 1000. but for now I can nail it off but I will try and see where we are supposed to be passing that option on,
Fixing this is likely to break stuff I am sure, so please dont fret, I'll submit a pull req in a bit if I can work out where we should be forwarding the pagination flag, and a variable for the 1000 max, but you might want a parent to be un-paginated and the child paginated, so care needed.

Add a Controller to a resource

After looking over the code and checking out #34, I couldn't find any support for this. I suppose I could manually require in the base controller class, instantiate it and then add it to the list... but that is less exciting.

Anyway, if you can even give me a hint how I'd add this feature I might do so and do a pull request.

I just want to be able to add custom endpoints to an existing resource so I can use the hooks and whatnot without having to basically remake that part of the library.

Also, if it's already in the library and I'm dumb, please tell me! :)

EDIT:
Example story: Say I have users with the usual scaffolding and want to add a special route to the resource (lets say it's GET users/chickens). I am fine with rolling my own logic for it, but I want to be able to add the controller to the resource so that epilogue does the router.get(blabla) for me, and exposes hooks on it.

epilogue swallows test assertion errors when the context returns an error

Add the following test to the end of milestones.test.js and run the test suite with USE_RESTIFY=1

it('should not swallow test exceptions when using restify', function(done) {
  var createErrorMiddleware = {
    create: {
      start: function(req, res, context) {
        return context.error(403, 'Forbidden', {});
      }
    }
  };
  var forcedErrorResource = rest.resource({
    model: test.models.User,
    endpoints: ['/exceptionTest', '/exceptionTest/:id']
  });
  forcedErrorResource.use(createErrorMiddleware);
  request.post({
    url: test.baseUrl + '/exceptionTest',
    json: {}
  }, function(error, response, body) {
    expect(1).to.equal(2);
    done();
  });
});

The test should error with a message stating that it expected 1 to equal 2. However, this exception gets swallowed somewhere and the test waits until it throws a timeout error. Running mocha with the debug options shows that chai makes it to the point where it should throw the error, but then execution just stops. I've recreated this test in restify itself and I couldn't get it to exhibit this behavior, so I'm convinced the issue is somewhere in epilogue. I'm at a loss. Any advice is appreciated.

Running node v0.12.4.

Criteria not added to context object

I was confused as to why the context.criteria object was always empty in read, as It seemed to me that it would make sense for it to contain the id parameter during a request to something like GET /api/users/1.

Well, it seems that context.criteria is never touched in the fetch method at all.

// lib/controllers/read.js
Read.prototype.fetch = function(req, res, context) {
  var model = this.model,
      endpoint = this.endpoint,
      criteria = {},
      include = this.include,
      includeAttributes = this.includeAttributes,
      options = {};
     ...
// later
      if (!instance) {
        res.status(404);
        context.error = { message: 'not found' };
        return context.continue();
      }

      context.instance = instance;
      return context.continue();

So, as you can see the context.criteria object is never assigned. I am not sure why this would be intentional so I assume it is a bug. I know in my case, it is essential to be able to extract the parameters in middleware for auth purposes.

You can check for yourself

I'll open a pull request later today as the fix seems trivial. I'll check the other controllers as well. Maybe I can find time to write tests as well, but that is more iffy.

Criteria is not accessible, when :id is invalid

Refrenced to #85

My API provides aliases for id. /users/me/ === /users/5, if current user's ID is 5. So, I've tried such approach:

users.read.fetch.before(function(req, res, context) {
  var userId = req.params.user_id

  if (userId === "me") {
    context.criteria.id = req.user.id
  }
  return context.continue;
})

Not working. Any ideas?

  1. this should work, can you please open a new issue so we can discuss this if there is an actual problem here.
var milestones = {
  users: require('./users.js')
}

var users = epilogue.resource({
  model: db.User,
  associations: true
});

users.use(milestones.users)
// ./users.js
module.exports = {
  read: {
    start: function(req, res, context) {
      console.log(context);
      return context.continue;
    },
    auth: function(req, res, context) {
      console.log(context);
      return context.continue;
    },
    fetch: {
      before: function(req, res, context) {
        console.log(context);
        return context.continue;
      }
    }
  }
}

Tesing

GET /api/v0/users/me

Output

{ instance: undefined,
  criteria: {},
  attributes: {},
  skip: [Function],
  stop: [Function],
  continue: [Function],
  error: [Function] }
{ instance: undefined,
  criteria: {},
  attributes: {},
  skip: [Function],
  stop: [Function],
  continue: [Function],
  error: [Function] }
{ instance: undefined,
  criteria: {},
  attributes: {},
  skip: [Function],
  stop: [Function],
  continue: [Function],
  error: [Function] }

Expected

criteria: {
  "id": "me"
}

UPD: Also tried to access to req object directly:

auth: authMiddleware.E.ensureAuthenticated,
fetch: {
  before: function(req, res, context) {
    console.log(context);
    // :me
    if (req.params.id === 'me') {
      context.criteria.id = req.user.id;
    }
    console.log(context);
    return context.continue;
  }
}

Response is:

{
  "message": "\"me\" is not a valid integer",
  "errors": []
}

The request doen't respect criteria: {} object, does it?

[question] Associations, criterias and milestones API

Hi, guys,

thank you for aweseome lib! I've recently started to play with epilogue, got some questions.

I've got two models: User and Folder. User.hasMany(Folder). So:

...
// Create REST resource
var userResource = epilogue.resource({
  model       : db.User,
  endpoints   : ['/users', '/users/:id']
});

1. Auto-associations?

Let's test:

curl http://192.168.59.103:8089/users/5/

{
  "id": 5,
  "email": "[email protected]",
  "hash": "...",
  ...
}

curl http://192.168.59.103:8089/users/5/folders/

Cannot GET /users/5/folders/

How to make epilogue handle related associations, but not to include them while ordinary request? I mean, I've tried

{
  accociations: [db.Folder]
}

and

{
  include: [db.Folder]
}

but epilogue includes [Folders] even in request to User node (/users/5). Instead if this I'd like epilogue to handle /users/5/folders, /users/5/folders/:id

2. Incorrect where attributes

My API provides aliases for id. /users/me/ === /users/5, if current user's ID is 5. So, I've tried such approach:

users.read.fetch.before(function(req, res, context) {
  var userId = req.params.user_id

  if (userId === "me") {
    context.criteria.id = req.user.id
  }
  return context.continue;
})

Not working. Any ideas?

3. Milestones API?

It seems midlestones API is a bit messy right now. Some snippets points for

users.read.fetch.BEFORE({...})

Some โ€” without before node:

users.read.auth({...})

Which is the proper way to use it? Which hooks are invoked for which controller? Any actual docs or the best option is to check the source?

BTW, I would be happy to help with docs as soon as I get this to work as expected myself :) Very helpful lib, just what I needed.

Association Routes?

Do you support association routes as well?
If I have a scenario for example..

Students have many courses
Each course has many semesters
Each semester has many subjects

and assuming the associations are defined correctly in Sequelize, can we auto generate routes like
POST /student/:studentId/course
GET /student/:studentId/course
PUT /student/:studentId/course/:courseId

POST /student/:studentId/course/:courseId/semester

POST /student/:studentId/course/:courseId/semester/:semesterId/subject

Promise over eventemitter - warning.

Great start with epilogue. I am understanding internals now and hopefully will be able to contribute.

Have you got a chance to get away from these warnings:

EventEmitter#success|ok is deprecated, please use promise-style instead.
EventEmitter#failure|fail|error is deprecated, please use promise-style instead.

express deprecated res.json(status, obj): Use res.status(status).json(obj) instead ../../node_modules/epilogue/lib/Controllers/read.js:45:13

Limit search to some columns (PostgreSQL error)

PostgreSQL doesn't like LIKE % queries on numeric fields, as every row has atleast a id, this is a problem.

The soultion would be to either only query string fields or allow field selection for full text search, below is a snippet that does the latter.

tags.list.fetch.before(function (req, res, context) {

    if (req.query.q) {
        var search = [];
        ['name', 'slug', 'category', 'description'].forEach(function(attr) {
            var item = {};
            item[attr] = { like: '%' + req.query.q + '%' };
            search.push(item);
        });

        context.criteria = models.sequelize.or.apply(null, search);
    }

    delete req.query.q;
    return context.continue();
});

great plugin, rock on

Modify request methods per resource?

(feature request)

I appreciate that you can specify your own updateMethod, but I'd like to the same for the create methods. Currently it's hard-coded as POST, but in my particular instance, PUT would be a better method. I'd like to be able to implement a route that can either create or update an instance using the upsert method in Sequelize, if that's possible.

Association not correct after UPDATE

epilogue.resource({
    model: db.Task,
    include: {
        model: db.User,
        as: "owner"
    }
});

After using a PUT /task/:id request to update the ownerId of a task object, the populated owner object in the json response points to the old owner, not the newly updated one.

Association Routes

Hi! Sort of new to sequelize, so this could be a problem with the way that I've setup my associations (I apologize in advance if that is the case). So far so good on getting this running with an existing database. I've got an issue with list on an association. Right now, I am doing this:

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('lots', {
    lot_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false,
      primaryKey: true
    },
    lot_event_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    lot_category_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    lot_number: {
      type: DataTypes.STRING,
      allowNull: false
    }
  });
};

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('events', {
    event_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false,
      primaryKey: true
    },
    event_user_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    event_auctioneer_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    event_lockcutter_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    event_event_category_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    event_calendar_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    }
  });
};

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('lot_images', {
    lot_image_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false,
      primaryKey: true
    },
    lot_image_event_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    lot_image_lot_id: {
      type: DataTypes.INTEGER(10),
      allowNull: false
    },
    lot_image_name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    lot_image_file: {
      type: DataTypes.STRING,
      allowNull: false
    },
    lot_image_main: {
      type: DataTypes.INTEGER(1),
      allowNull: false
    },
    lot_image_deleted: {
      type: DataTypes.BOOLEAN,
      allowNull: false
    }
  });
};

events.hasMany(lot_images, {
  foreignKey: 'lot_image_event_id',
  constraints: false,
});

events.hasMany(lots, {
  foreignKey: 'lot_event_id',
  constraints: false,
});

// Initialize epilogue
epilogue.initialize({
  app: app,
  sequelize: sequelize
});


// Create REST resource
var eventsResource = epilogue.resource({
  model: events,
  associations: true,
  endpoints: ['/events', '/events/:event_id', 'events/:event_id/lot_images', 'events/:event_id/lots'],
  actions: ['read', 'list'],
});    

I'd like for the /events endpoints to return just the events records from the database , but right now when using list /events resource it returns EVERYTHING for the events including all lots & lot_images for each event. How do I get this to just return the events resource without the associations in the response? P.S. I did not architect the DB structure.

associations vs include

I figured out that the auto-associations branch is already included in the master.

When using it and having the include and associations option turned on, I get this error when using list on the resource:

`"SQLITE_ERROR: ambiguous column name: Users.id"

Removing the include option makes it work.
So it seems to me include and associations do not work together.

I do not understand why it has to be this way, because I thought associations will make the the routes for the associations available and will provide the association function for the resource.

Why should it make include useless? Maybe I want the associations to work, but still control what is returned vial list?

My main problem is, that via associations I cannot control the attributes that are returned like I can in include

Am I just using it wrong?

A full example

Would you be willing to provide a full working code example?

I'm attempting to use Epilogue with Sequelize with Express and there are conflicting conventions for how to organize models and controllers (or lack thereof).

So far I can get working code from the express and sequelize examples, but not with Epilogue. I'm sure I'm just not cutting out the right parts of the parent applications since epilogue handles much of the functionality in those examples.

It would be very helpful if the readme had a full code example for a simple app like a modification of the the task list example Sequelize gives.

Help on context / milestones

I have some trouble figuring out where I should plug myself to implement security on all my routes.

My usecase is pretty trivial at the moment:

  • User: that is used to auth via passport
  • List: belongs to User, and having many Item
  • Item

How would you implement security on those routes for instance ?

  • GET /items/ : it should return all items this connected user has
  • PUT /items/:id : it should throw 403 when item does not belong to connected user

Thanks !

/<parent entity>/:parent_id/<entity>/[:id]

sequelize-restful just bit the dust with the latest sequelize.
He told me to come here.
I was using /parent entity/:parent_id/entity/[:id]
that's not listed in the instructions.
Do I need to implement this / salvage from sequelize-restful ?

Case sensitivity in model queries

Hey!

The documentation suggests that the standard q query functionality performs a case-insensitive substring search (ie: resource?q=james returns an object where name:"James Conrad"). In practice, I have not found this to be the case.

For instance, resource?q=vin returns records with name:"Alvin", where resource?q=Vin yields an empty set.

I'm using the postgres dialect. Here are the relevant libraries/versions:

  "dependencies": {
    "epilogue": "^0.5.1",
    "express": "~4.12.4",
    "pg": "^4.4.0",
    "pg-hstore": "^2.3.2",
    "restify": "^3.0.3",
    "sequelize": "^3.3.0",
    "sequelize-cli": "^1.7.0"
  }

Why is endpoint configuration specified as an array?

Creating a resource requires specifying a list of endpoints as an array. Digging into the code, it seems that in reality, you can only provide two endpoints, and their position has semantic meaning (the first endpoint is the plural, second is the singular).

If this is true, why not just specify the endpoints as an object? I dug into this because the array led me to believe there were other options (i.e. RPC style endpoints perhaps).

Happy to submit a pull request with the change, just thought I'd check first to see if there was a rationale I'm missing.

"include" causes 404 response if an included model has a "where" clause with no results

On a normal GET request for all instances or a single instance, a 404 Not Found is returned if the resource is defined with an include as well as a "where" clause, and no results satisfy that "where" clause.

Removing the "where" clause allows the instance(s) to be returned even if no data is found for the included model.

For example, for an internet radio station API, a Channel, can have several Streams, as well as Metadata (station track history) for which I am returning the last hour of when a request comes in for channels.

var channels = rest.resource({
  model: models.Channel,
  include: [
    models.Stream,
    {
      model: models.ChannelMetadata,
      where: Sequelize.or({
        endTime: {
          gt: new Date(new Date() - 60 * 60 * 1000) // 1 hour ago
        }
      }, {
        endTime: null // Current metadata won't have an end time
      })
    }
  ],
  endpoints: ['/channels', '/channels/:id'],
  actions: ['list', 'read']
});

Again, removing the "where" object here causes everything to work fine, even if no Metadata is associated with the requested Channel.

Resulting query without "where" clause (returns results):

SELECT "Channel".*,
       "Streams"."id" AS "Streams.id",
       "Streams"."url" AS "Streams.url",
       "Streams"."contentType" AS "Streams.contentType",
       "Streams"."averageBitrate" AS "Streams.averageBitrate",
       "Streams"."createdAt" AS "Streams.createdAt",
       "Streams"."updatedAt" AS "Streams.updatedAt",
       "Streams"."ChannelId" AS "Streams.ChannelId",
       "ChannelMetadata"."id" AS "ChannelMetadata.id",
       "ChannelMetadata"."title" AS "ChannelMetadata.title",
       "ChannelMetadata"."startTime" AS "ChannelMetadata.startTime",
       "ChannelMetadata"."endTime" AS "ChannelMetadata.endTime",
       "ChannelMetadata"."ChannelId" AS "ChannelMetadata.ChannelId"
FROM
  (SELECT "Channel"."id",
          "Channel"."name",
          "Channel"."description",
          "Channel"."createdAt",
          "Channel"."updatedAt"
   FROM "Channels" AS "Channel" LIMIT 100) AS "Channel"
LEFT OUTER JOIN "Streams" AS "Streams" ON "Channel"."id" = "Streams"."ChannelId"
LEFT OUTER JOIN "ChannelMetadata" AS "ChannelMetadata" ON "Channel"."id" = "ChannelMetadata"."ChannelId";

Resulting with "where" clause:

SELECT "Channel".*,
       "Streams"."id" AS "Streams.id",
       "Streams"."url" AS "Streams.url",
       "Streams"."contentType" AS "Streams.contentType",
       "Streams"."averageBitrate" AS "Streams.averageBitrate",
       "Streams"."createdAt" AS "Streams.createdAt",
       "Streams"."updatedAt" AS "Streams.updatedAt",
       "Streams"."ChannelId" AS "Streams.ChannelId",
       "ChannelMetadata"."id" AS "ChannelMetadata.id",
       "ChannelMetadata"."title" AS "ChannelMetadata.title",
       "ChannelMetadata"."startTime" AS "ChannelMetadata.startTime",
       "ChannelMetadata"."endTime" AS "ChannelMetadata.endTime",
       "ChannelMetadata"."ChannelId" AS "ChannelMetadata.ChannelId"
FROM
  (SELECT "Channel"."id",
          "Channel"."name",
          "Channel"."description",
          "Channel"."createdAt",
          "Channel"."updatedAt"
   FROM "Channels" AS "Channel"
   WHERE
       (SELECT "ChannelId"
        FROM "ChannelMetadata" AS "ChannelMetadata"
        WHERE "Channel"."id" = "ChannelMetadata"."ChannelId"
          AND ("ChannelMetadata"."endTime" > '2015-01-02 21:49:25.414 +00:00'
               OR "ChannelMetadata"."endTime" IS NULL) LIMIT 1) IS NOT NULL LIMIT 100) AS "Channel"
LEFT OUTER JOIN "Streams" AS "Streams" ON "Channel"."id" = "Streams"."ChannelId"
INNER JOIN "ChannelMetadata" AS "ChannelMetadata" ON "Channel"."id" = "ChannelMetadata"."ChannelId"
AND ("ChannelMetadata"."endTime" > '2015-01-02 21:49:25.414 +00:00'
     OR "ChannelMetadata"."endTime" IS NULL);

Error formating / Docs

I'm trying to follow docs/Milestones.md in order to change the error output of my APIs.

A few questions :

  1. is there a way to globally update the Error JSON output ({message, errors}) or is it only possible per-action ?
  2. Secondly : should the following code work ? If yes, do you have a clue why it doesn't ?
var resource = epilogue.resource({
    model: models.test,
    endpoints: ['/test', '/test/:id']
});

resource.create.error = function(req, res, error) {
    res.status(500);
    res.json({message: 'Custom error'});
};

var resourceMiddlewares = {
    create: {
        write: {
            before: function(req, res, context) {
                throw new epilogue.Errors.BadRequestError("just fail please");
                return context.continue();
            }
        }
    }
};
  1. I struggled to have a working "create.write.before" hook, the docs aren't clear :
var resource = epilogue.resource({
    model: models.test,
    endpoints: ['/test', '/test/:id']
});
resource.create.write.before(function() {[...]}); //doesn't work
resource.use({create:{write:{before: function() {[...]}}}}) //works but not documented

Not sure If I understood all well though :o

Thanks

About Query was empty error

Hi;

First I wrote this issue to SequelizeJS, but I think the problem can be related with epilogue.

sequelize/sequelize#2850

I debug the code of epilogue and I think, there can be a bug in the line:

  _(context.attributes).extend(_(req.body).clone());

in update.js

I am making PUT request to my server with data:

{"id":87,"name":"name1","description":"description1","MenuId":63}

If I submit these values in a form in Postman, it is successful.

If I send raw data, req.body is empty and I am getting this error:

{"error":{"name":"SequelizeDatabaseError","message":"ER_EMPTY_QUERY: Query was empty","parent":{"code":"ER_EMPTY_QUERY","errno":1065,"sqlState":"42000","index":0,"sql":""},"original":{"code":"ER_EMPTY_QUERY","errno":1065,"sqlState":"42000","index":0,"sql":""},"sql":""}}

Context criteria and attributes are always empty objects

I am trying to figure out how to authenticate a request based on what Epilogue has determined the criteria are. I thought it would make sense to use the hooks and milestones for that.

The context object has criteria and attributes properties. While their functionality isn't documented, criteria is used in one of the code examples. I'm assuming that one of them is supposed to contain any query criteria as parsed from the request.

If I pass in a request for /streams?AccountId=1 (where AccountId is actually for an association to an Account model), I was hoping to be able to access this somewhere as Epilogue would use it in a query. While it is possible for me to pull this out of the query string on req myself, if I can validate the criteria Epilogue uses in its own query, I believe that would be a more reliable and secure implementation.

Is this possible today?

function tracer(req, res, context) {
  console.log('context', context);
  return context.continue();
}

rest.resource({
  model: orm.models.Stream,
  endpoints: ['/streams', '/streams/:id'],
  include: [ orm.models.Server ]

}).use({
  list: {
    start:  tracer,
    auth: tracer,
    fetch: tracer,
    data: tracer,
    write: tracer,
    send: tracer,
    complete: tracer
  }
});

Best practice for moving code out of app.js or server.js

Do you move the code for creation of the resources for each Model into a classMethod of that model?
I would not want to do that but have all the REST API epilogue logic such as middleware separated from the model definition and I am fine to call it "manually" from app.js. Like with models one file per resource would be nice, I guess.

How do you guys handle this?

how to limit attributes on resource?

when I do a list or read, how do I limit the attributes to the one I want?

I don't want to send the password of a user to begin with.

I know I can specify the attributes when I include another resource, but what about the plain resource when read/list itself?

I am sure it has to be possible but I don't seem to be seeing it.

Or do I have to use a hook/milestone and delete the fields before sending? This seems to be the wrong approach.

thanks for your help!

How can I retrieve Many-to-Many data

Hi;

I have many-to-many categories-items relation.

I want to retrieve the items that belongs to a category.

I make a request with "/items?CategoryId=1"

But I am getting

"ER_BAD_FIELD_ERROR: Unknown column 'Item.CategoryId' in 'where clause'"

because relationship is saved in category_items table.

How can I retrieve the associated data in many-to-many relationships.

Exclude some fields from response

Hi,

I would like to exclude some fields from the response. I did not find a clear method for that.
At the moment, for the read method, I delete fields from instance.dataValues on the middleware read.send.before. That work well however if it is a getter for this field, I still get the value returned by the getter.
Is it a better way to do that ?

Consider a koa version

koa is a superior alternative to Express. You might want to consider implementing a version of epilogue compatible with it.

Search on associations

Suppose I have a database of music with separate models for tracks, artists, and albums. I want to be able to search tracks based on artist name, album name, etc. Is there a built-in way to do this in Epilogue? For example, it would be nice to be able to use the q parameter to hit the associations:

  epilogue.resource({
    model: models.Track,
    endpoints: ['/tracks', '/tracks/:id'],
    actions: ['read', 'list'],
    search: {
      attributes: ['title', 'Artist.name']
    },
    include: [
      {
        model: models.Artist,
        attributes: ['id','name']
      },{
        model: models.Album,
        attributes: ['id','title']
      }
    ]
  });

It would also be helpful to use: /tracks?Artist.name="somebody". Is this possible today? If not, is there a recommended method to use Epilogue or do we have to do this entirely ourselves? I would like to take advantage of Epilogue's handling of pagination and sorting if possible.

add ability to pass in "include" to context for relational mapping in list results

Hi,
I tried to do this on my own tonight and failed. I wanted to be able to something like this:

var users = rest.resource({
    model: database[model],
    endpoints: ['/rest/users', '/rest/users/:id']
});

users.list.fetch.before(function(req, res, context) {
    context.include = [ Address ]
    context.continue();
});

the idea being that if you have a relation between the two models (say Address.belongsTo(User) for instance), you could modify the list method to return something like:

{
    name: "John",
    email: "[email protected]",
    address: {
        something: "or other";
    }
}

It was easy enough to add the "include" property to context and inject it into the "findAll" in list.js, however, when I actually ran the unit test I put together it said that users and addresses were not related. I had basically copied your tests/resource.js and replaced the hasMany relation with a belongsTo, and that's about where I quit.

Any ideas how to get this working? I would provide a patch but its literally something like four lines of code.

TypeError: Object User has no method 'findAndCountAll'

What wrong I am doing with this code:

var rest = require('epilogue');
var express = require('express');
var models = require("../models");

var app = express();

rest.initialize({
app: app,
sequelize: models.sequelize
});

var users = rest.resource({
model: "User",
endpoints: ['/users', '/users/:id']
});

app.listen(3002, function() {
console.log("โœ” Express server listening on port %d in %s mode", 3002, app.get('env'));
});

I have created user model as :

"use strict";

module.exports = function(sequelize, DataTypes) {
var User = sequelize.define("User", {
username: DataTypes.STRING,
password: DataTypes.STRING,
active: DataTypes.BOOLEAN
}, {
classMethods: {
associate: function(models) {
User.hasMany(models.Post)
}
}
});

return User;

};

Using index.js I am initialising all models present in the models folder.

I believe the problem is with this line: model: "User", but I don't know how to get the model here now.

I am getting this error:

TypeError: Object User has no method 'findAndCountAll'
at List.fetch (//Documents/gits/tm/node_modules/epilogue/lib/Controllers/list.js:89:6)

API interface description

Sequelise-restful had a method of requesting, via the HEAD of /api/<entity> essentially a JSON.stringify of the relevant sequelize.model. It didnt really work, once you added the associations there was a circular reference and it bombed. But also, I dont really want to have to first call /api/ which gave back an array of the entities it knows about, and then call HEAD on /api/entity for each of them.
So I have this ugly thing below, which prints out all the models and skips any object keys starting with _ and a messy list of suspects.
It's not something that needs to be within Epilogue, and certainly as it stands it's highly bespoke, I am just touching base that this isn't something Epilogue does, or wants to do.
But having just said all that, it would be nice to actually operate on these resources I now make with Epilogue, and print them out, along with their sequelize.model and thus get the end points to assign to url and urlRoot in my Backbone Model and Collection extensions which I generate from the above.
So that would make a nice and tidy epilogue-dictionary package, as long as we can work out a neat way of dumping sequelize models.
FYI, my quest is a thing called WikiTimetable, which is an open access editor for GTFS https://developers.google.com/transit/gtfs/reference?hl=en . I picked this up again in August having abandoned my initial system (which worked, but not really). I've spent the last 10 weeks discovering node and cobbling this together https://github.com/mark-lester/Gemini, which is basically my application code but I am trying to factor out as much as possible.

restDictionary:function(){
    var self=this;
    return function(req, res){
        res.setHeader('Content-Type', 'application/json');
        var models=_.filter(self.models, function(model){
                return model.Gemini.excludeFromApi !== true;
            });

        res.send(JSON.stringify({
                                 models
        },function(k,v){
            if (k.charAt(0) === '_')
                return undefined;
            if (typeof v === 'object')
                switch(k){
                    case 'classMethods':
                    case 'instanceMethods':
                    case 'modelManager':
                    case 'Model':
                    case 'daoFactoryManager': 
                    case 'daos': 
                    case 'source': 
                    case 'target': 
                    case 'sequelize':
                    case 'GeminiHidden':
                    case 'hooks':
                    case 'lookup':
                        return undefined;
                }
            return v;
        }));
    };
},

Automatic documentation

Are there any good libraries out there for automatically generating documentation based on epilogue resources?

Model is not associated to another model!

Hi;

I have following models in sequelize js:

module.exports = function(sequelize, DataTypes) {
var Menu = sequelize.define('Menu', {
   order: DataTypes.INTEGER,
   published: DataTypes.BOOLEAN,
}, {
    associate: function(models) {
        Menu.belongsTo(models.Theme, {as: 'Theme', constraints:false});
    },
    tableName: 'menus'
});
return Menu;
};

module.exports = function(sequelize, DataTypes) {
var Theme = sequelize.define('Theme', {
    name: DataTypes.STRING,
    description: DataTypes.STRING,
}, {
    tableName: 'themes'
});
return Theme;
};

My epilogue configuration for menu is:

var menus = rest.resource({
  model: db.Menu,
  include: [{
    model: db.MenuTranslation,
    include: [db.Language]
  }, db.Theme],
  endpoints: ['/menus', '/menus/:id']
});

I am getting "Theme is not associated to Menu!". Is there something missing?

Is this module using Express or Restify?

Hello,

I'm newbie in Node.js and RESTful APIs using this project, but your READ says:

Create flexible REST endpoints and controllers from Sequelize models in your Express app in Node.

However, the main demo uses Restify:

var Sequelize = require('sequelize'),
    restify = require('restify'), // << here!
    epilogue = require('epilogue');
...

So, is this module using Express or Restify? =/

Thank you!

Filter by boolean value not possible as it is interpreted as string

Given the model:

var Task = sequelize.define("task", {
    archived: Sequelize.BOOLEAN
});

The following epilogue route:

/epilogue/tasks?archived=false

does not work. It throws:

{"message":"\"false\" is not a valid boolean","errors":[]}

It seems the value is interpreted as a string and never converted.

Error Formatting

Defaults formatting

Errors should be in the format:

{
  "message": "error message",
  "errors": ["array", "of", "problems"]
}

The following show just 'message' when returning an error:

https://github.com/dchester/epilogue/blob/master/lib/Controllers/read.js#L45
https://github.com/dchester/epilogue/blob/master/lib/Controllers/delete.js#L30

I don't think these are a problem, the errors array should be optional.

This file is just showing error for a 404 though:
https://github.com/dchester/epilogue/blob/master/lib/Controllers/update.js#L33

Plus this line just returns error for a 500:
https://github.com/dchester/epilogue/blob/master/lib/Controllers/list.js#L120

These should probably use a similar system to stop having to worry about fitting the same format each time.

Returning Errors

Returning an error is awkward from milestones too, for example I have this middleware:

module.exports = function(attributes) {
  if(!_.isArray(attributes)) attributes = [attributes];

  return function(req, res, context) {
    var intersect = _.intersection(_.keys(req.body), attributes);
    if(intersect.length) {
      res.status(400);
      context.error = {
        message: 'Cannot set fields on this object',
        errors: intersect
      };
      // skip the actual write
      return context.skip();
    }

    return context.continue();
  };
};

Here I am using the same system used in the default middlewares of doing:

res.staus(400); //set error code
context.error = {
  message: 'error message',
  errors: ['array of problems']
}
context.skip(); // skip to next

The problem I have with this is it is now required to be ran on write.before so the skip works and then the write function needs to also check there is no errors to context.continue. On create the default write function doesn't check context.error but the one in update does. Every middleware called after this one needs to be aware of the error to avoid processing when there is an error and allow the error to pass through to the deafult .send action (on this note the complete callback says Run the specified function when the request is complete, regardless of the status of the response. but as far as I can tell if context.stop() is called it won't run)

Potential Solution

I think a proper way to handle this would be to add error to context as a function.

context.error({
  message: 'error message',
  status: 400,  // optional and default 500?
  errors: ['array of problems'] // optional
});

Calling error should then call a special 'error' function that is outside of the chain (and probably no .before or .after) where the default can be:

function(req, res, context, error) {
  res.status(error.status);
  res.json(_.omit(error, ['status']));
  context.stop();
}

This would make keeping to the default format within epilogue simple as they would just need to call context.error(...) and would no longer need to check context.error and continue if it is set. It would also allow customization of the default middleware functions error rendering.

It should also allow context.continue() and context.skip() to resume processing from within the error milestone.

It could also be tier structured:

Running create.write.after would check for and run the first of:

model.create.write.after.error  
model.create.write.error  
model.create.error  
model.error  
default error function  

And update.fetch would check for

model.update.fetch.action.error
model.update.fetch.error
model.update.error
model.error
default error function

If the tiering isn't possible at minimum a 'previous state' variable should be accessible in the error function to tell what hook errored and allow separate processing based on the state

On another side I also see this:
https://github.com/dchester/epilogue/blob/master/lib/Controllers/base.js#L122

Which should catch any uncaught errors, but just passes them to an empty function

Multiple includes with a 1:1 association and M:N association.

Consider the following test (sorry, it's huge, but it's very specific):

describe('Multiple Includes', function() {
  before(function() {
    test.models.Address = test.db.define('addresses', {
      id: { type: test.Sequelize.INTEGER, autoIncrement: true, primaryKey: true },
      street: { type: test.Sequelize.STRING }
    }, {
      underscored: true,
      timestamps: false
    });

    test.models.Person = test.db.define('person', {
      id: { type: test.Sequelize.INTEGER, autoIncrement: true, primaryKey: true },
      name: { type: test.Sequelize.STRING, unique: true }
    }, {
      underscored: true,
      timestamps: false
    });

    test.models.Hobby = test.db.define('hobby', {
      id: { type: test.Sequelize.INTEGER, autoIncrement: true, primaryKey: true },
      name: { type: test.Sequelize.STRING }
    }, {
      underscored: true,
      timestamps: false
    });

    test.models.Person.belongsTo(test.models.Address, {
      as: 'addy',
      foreignKey: 'addy_id'
    });
    test.models.Person.belongsToMany(test.models.Hobby, {
      as: 'hobbies',
      through: 'person_hobbies'
    });
    test.models.Hobby.belongsToMany(test.models.Person, {
      as: 'people',
      through: 'person_hobbies'
    });
  });

  beforeEach(function(done) {
    test.initializeDatabase(function() {
      test.initializeServer(function() {
        rest.initialize({
          app: test.app,
          sequelize: test.Sequelize
        });

        rest.resource({
          model: test.models.Person,
          include: [
            { model: test.models.Address, as: 'addy' },
            { model: test.models.Hobby, as: 'hobbies' }
          ],
          endpoints: ['/personWithTwoIncludes', '/personWithTwoIncludes/:id']
        });

        done();
      });
    });
  });

  it('should be ok', function(done) {
    test.models.Person.create({ name: 'testName' })
      .then(function() {
        request.get({
          url: test.baseUrl + '/personWithTwoIncludes'
        }, function(error, response, body) {
          expect(response.statusCode).to.equal(200);
          done();
        });
      });
  });
});

The body returned by the request is:
{"message":"internal error","errors":["SQLITE_ERROR: no such column: person.addy_id"]}

This is because we filter out related fields in our attributes list inside the list controller. It is only a problem in this scenario because sequelize generates a subquery when it has a M:N relationship and a limit, and us omitting addy_id from our attributes confuses it. The easiest solution is to not omit associated fields, but then we end up with extraneous data in our response body (here, we'd have a body.Addy object as well as a body.addy_id field).

Thoughts?

Nested route

Hi,

How can I achieve nested route such as /users/activities and get those associated values.
If it is not possible then how can get sequalizer to do it for me ?

Cheers,
amit

Option to disable pagination

It might be interesting to have an option to disable pagination, an retrieve all objects (instead of the 1000 harcoded limit), for each resource.

For example :

var resource = epilogue.resource({
        model: models.categories,
        endpoints: ['/category', '/category/:id'],
        actions: ['create', 'list', 'read', 'update'],
        pagination: false
});

Validating JSON output before sending ?

Hello

I'm having a few problems when trying to validate the JSON output. How would you do that for a specific route (LIST) ?

I tried to add my validation routine (using ajv) in myApi.list.data.after(function(){}); without success :

  • should I be doing this in list.data.after or list.send.before ?
  • is it normal that list.data.after() is never called but list.data() is called (I though the logic was ...<before|action|after>) ?
  • how can I have access to the generated JSON ?

Thanks for you help and this awesome module !

error design

Hi guys, this is more of a discussion "issue". I wanted to work out detailed rules for what our errors look like in epilogue. As of right now we have a few cases:

  • If ever an instance is not found, we return a status code 404, and an error object (though this differs in a number of places in terms of implementation):
HTTP/1.1 404
{ error: 'not found' }
  • If a validation error occurs we return a status code 400 and an error object that kind of looks like:
HTTP/1.1 400
{
  error: {
     < full sequelize ValidationError error object >
  }
}
  • If a unique constraint error occurs, we return 400 and the sequelize DatabaseErrorObject
HTTP/1.1 400
{
   error: {
       < full sequelize DatabaseError object >
   }
}
  • If anything generally goes wrong we return a 500 status code and whatever error pops up from sequelize:
HTTP/1.1 500
{
    error: {
        < some sequelize Error object, probably a DatabaseError object >
    }
}

It seems that at least we are consistent with returning the "error" object, but that perhaps we could help users of epilogue out by formalizing the error object a little more. I propose we stick to a standard error object for all types of errors:

HTTP/1.1 400 or 500
{
    error: {
        message: 'some message',
        fields: {
            // if they exist for e.g. validation errors
        }
    }
}

I think this is a better approach for a couple of reasons:

  1. We never respond with the full sequelize error, which in many cases has the full attempted sql statement (which you don't want to expose to outsiders)
  2. Users of the api can always count on at least an 'error.message' being there, rather than having to check whether the result of an action was a string or an object

Alternatively, we could go with a far more "RESTful" approach and try to convey the information as much as possible through status code and headers. In this case we could for instance:

  • assume that anything in the 400's or 500's is an error, so...
  • omit the error object completely, ending up with something like:
HTTP/1.1 400 or 500
{
    message: 'some message',
    fields: {
        // if validation errors occurred
    }
}

What do you guys think?

Body parser is missing, Epilogue fails on a bare Express 4.x app without it

When Epilogue is used in an application that doesn't already use body-parser, it fails to process JSON out of a POST body causing all fields to be undefined.

I worked around this in my application by adding it at the application level, but I believe this should be added to Epilogue directly so that Epilogue can stand alone. Example: http://stackoverflow.com/a/27855234/362536

In addition, while tracking down this issue I spent a good amount of time assuming my POST body was incorrect in some way. I found that there was no example outside of the unit tests for what to POST to Epilogue. Perhaps the README should have a basic example?

Problem when trying to add a custom middleware function in addition of epilogue middleware (on top of restify)

Hi guys,

Thank you for building this useful lib :) ! I recently decided to use it (version 0.5.2) in a project with restify (4.0.0).

I encounter some problem when I try to add a simple middleware function that allows CORS. If I don't add my middleware function, call to the resources return the expected result, i.e., this code works (no surprise!) :

var server = restify.createServer();
server.use(restify.queryParser());
server.use(restify.bodyParser());

epilogue.initialize({
  app: server,
  sequelize: sequelize // defined somewhere else
});

var employeeResource = epilogue.resource({
  model: employee // defined somewhere else
});

server.listen(8080);

Now, things go much more worse if I add the function. Description of symptoms :

1- This code below (that adds my middleware function before epilogue initialization)) makes the call to the ressource returns {"code":"InternalError","message":""name" and "value" are required for setHeader()."}

var server = restify.createServer();
server.use(restify.queryParser());
server.use(restify.bodyParser());

server.use(function (req, res, next) {
   res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
   res.setHeader('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type');
   next();
});

epilogue.initialize({
  app: server,
  sequelize: sequelize // defined somewhere else
});

var employeeResource = epilogue.resource({
  model: employee // defined somewhere else
});

server.listen(8080, () => {
    console.log('listening at %s', server.url);
});

2 - If I add my middleware function after epilogue initialization, it avoids symptom (1), but the Access-Control-Allow-Origin header is not added to the response, making my call fail with the traditionnal javascript error "XMLHttpRequest cannot load http://localhost:8080/employees. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin [...] is therefore not allowed access."

var server = restify.createServer();
server.use(restify.queryParser());
server.use(restify.bodyParser());

epilogue.initialize({
  app: server,
  sequelize: sequelize // defined somewhere else
});

var employeeResource = epilogue.resource({
  model: employee // defined somewhere else
});

server.use(function (req, res, next) {
   res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
   res.setHeader('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type');
   next();
});

server.listen(8080, () => {
    console.log('listening at %s', server.url);
});

3- If I use two variables to store reference to restify object (like I found in some example samples of epilogue) and declare my middleware function before epilogue stuffs, it works for a valid request, but return CORS error for a wrong one (I tested to call an unknown resource).

NB : This pattern with the use of two variables seems very weird from the point of view of users :)...it does not sound like a very good practice for an API. Would it be a way to make epilogue evolve to avoid the side effects that makes this pattern mandatory ? If such a way exists (must be ^^), maybe this NB would go in the pull requests list !

var app, server;
app = server = restify.createServer();
app.use(restify.queryParser());
app.use(restify.bodyParser());

server.use(function (req, res, next) {
   res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
   res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
   res.setHeader('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type');
   next();
});

epilogue.initialize({
  app: app,
  sequelize: sequelize // defined somewhere else
});

var employeeResource = epilogue.resource({
  model: employee // defined somewhere else
});

server.listen(8080, () => {
    console.log('listening at %s', server.url);
});

I have the feeling that this might come from a miscall of the next() continuation function inside epilogue.

Thank you in advance for your answers !
Cheers

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.