9 Commits

Author SHA1 Message Date
5ba9d7d261 2.9.1 2016-11-01 19:10:04 -07:00
07837ef36c Merge pull request #33 from mdibaiee/add-limit-offset-tests
Add integration tests for limit/offset
2016-11-01 19:09:45 -07:00
25501bbb10 Test add integration tests for limit/offset
#28
2016-11-01 17:39:35 -07:00
a335471f02 Fix(crud/list) 404 on no results 2016-11-01 17:39:16 -07:00
ce26814f74 Test add a returnsAll scope to player fixture 2016-11-01 17:38:55 -07:00
d1fc6d46e8 2.9.0 2016-10-31 14:24:12 -07:00
4e94c7f825 Merge pull request #32 from mdibaiee/more-order-integration
Feat ordering by associated models now works
2016-10-31 14:23:43 -07:00
c289fb2ed4 Feat ordering by associated models now works
It's now possible to order by associated models. This technically might
have worked before b/c we were parsing JSON sent to `order`, but I'm
pretty sure it wouldn't actually work b/c we never grabbed the actual
model to associate by. Regardless, this actually enables things and adds
tests to prove it.

Note: there's a sequelize bug that's poorly reported but definitely
known where `order` with associated models can fail because the sql
generated doesn't include a join. So, I added docs noting that and a
`test.failing` so that we'll be notified when that bug is fixed and can
remove the note.
2016-10-31 12:48:34 -07:00
e1b851f932 Test: add a second mock team
So that we can test complex sorts
2016-10-31 12:41:34 -07:00
9 changed files with 235 additions and 34 deletions

View File

@ -172,6 +172,22 @@ Team.findAll({order: [['name', 'DESC']]})
Team.findAll({order: [['name'], ['city']]})
```
You can even order by associated models. Though there is a [sequelize bug](https://github.com/sequelize/sequelize/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20order%20join%20) that might prevent this from working properly. A workaround is to `&include` the model you're ordering by.
```js
// returns the players ordered by the team name
// GET /players?order[0]={"model": "Team"}&order[0]=name
// results in a Sequelize query:
Player.findAll({order: [[{model: Team}, 'name']]})
// if the above returns a Sequelize error: `No such column Team.name`,
// you can work around this by forcing the join into the query:
// GET /players?order[0]={"model": "Team"}&order[0]=name&include=team
// results in a Sequelize query:
Player.findAll({order: [[{model: Team}, 'name']], include: [Team]})
```
## Authorization and other hooks
You can use Hapi's [`ext` option](http://hapijs.com/api#route-options) to interact with the request both before and after this module does. This is useful if you want to enforce authorization, or modify the request before or after this module does. Hapi [has a full list of hooks](http://hapijs.com/api#request-lifecycle) you can use.

View File

@ -1,6 +1,6 @@
{
"name": "hapi-sequelize-crud",
"version": "2.8.0",
"version": "2.9.1",
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js",
"config": {

View File

@ -0,0 +1,94 @@
import test from 'ava';
import 'sinon-bluebird';
import setup from '../test/integration-setup.js';
const STATUS_OK = 200;
const STATUS_NOT_FOUND = 404;
setup(test);
test('/players?limit=2', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players?limit=${limit}`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, limit);
});
test('/players?limit=2&offset=1', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players?limit=${limit}&offset=1`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, limit);
});
test('/players?limit=2&offset=2', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players?limit=${limit}&offset=2`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, 1, 'with only 3 players, only get 1 back with an offset of 2');
});
test('/players?limit=2&offset=20', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players?limit=${limit}&offset=20`;
const method = 'GET';
const { statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_NOT_FOUND, 'with a offset/limit greater than the data, returns a 404');
});
test('scope /players/returnsAll?limit=2', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players/returnsAll?limit=${limit}`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, limit);
});
test('scope /players/returnsAll?limit=2&offset=1', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players/returnsAll?limit=${limit}&offset=1`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, limit);
});
test('scope /players/returnsAll?limit=2&offset=2', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players/returnsAll?limit=${limit}&offset=2`;
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
t.is(result.length, 1, 'with only 3 players, only get 1 back with an offset of 2');
});
test('scope /players/returnsAll?limit=2&offset=20', async (t) => {
const { server } = t.context;
const limit = 2;
const url = `/players/returnsAll?limit=${limit}&offset=20`;
const method = 'GET';
const { statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_NOT_FOUND, 'with a offset/limit greater than the data, returns a 404');
});

View File

@ -9,7 +9,7 @@ setup(test);
test('/players?order=name', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = instances;
const { player1, player2, player3 } = instances;
const url = '/players?order=name';
const method = 'GET';
@ -18,11 +18,12 @@ test('/players?order=name', async (t) => {
// this is the order we'd expect the names to be in
t.is(result[0].name, player1.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player3.name);
});
test('/players?order=name%20ASC', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = instances;
const { player1, player2, player3 } = instances;
const url = '/players?order=name%20ASC';
const method = 'GET';
@ -31,24 +32,26 @@ test('/players?order=name%20ASC', async (t) => {
// this is the order we'd expect the names to be in
t.is(result[0].name, player1.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player3.name);
});
test('/players?order=name%20DESC', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = instances;
const { player1, player2, player3 } = instances;
const url = '/players?order=name%20DESC';
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
// this is the order we'd expect the names to be in
t.is(result[0].name, player2.name);
t.is(result[1].name, player1.name);
t.is(result[0].name, player3.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player1.name);
});
test('/players?order[]=name', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = instances;
const { player1, player2, player3 } = instances;
const url = '/players?order[]=name';
const method = 'GET';
@ -57,19 +60,74 @@ test('/players?order[]=name', async (t) => {
// this is the order we'd expect the names to be in
t.is(result[0].name, player1.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player3.name);
});
test('/players?order[0]=name&order[0]=DESC', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = instances;
const { player1, player2, player3 } = instances;
const url = '/players?order[0]=name&order[0]=DESC';
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
// this is the order we'd expect the names to be in
t.is(result[0].name, player2.name);
t.is(result[0].name, player3.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player1.name);
});
// multiple sorts
test('/players?order[0]=active&order[0]=DESC&order[1]=name&order[1]=DESC', async (t) => {
const { server, instances } = t.context;
const { player1, player2, player3 } = instances;
const url = '/players?order[0]=name&order[0]=DESC&order[1]=teamId&order[1]=DESC';
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
// this is the order we'd expect the names to be in
t.is(result[0].name, player3.name);
t.is(result[1].name, player2.name);
t.is(result[2].name, player1.name);
});
// this will fail b/c sequelize doesn't correctly do the join when you pass
// an order. There are many issues for this:
// eslint-disable-next-line
// https://github.com/sequelize/sequelize/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20order%20join%20
//
// https://github.com/sequelize/sequelize/issues/5353 is a good example
// if this test passes, that's great! Just remove the workaround note in the
// docs
// eslint-disable-next-line
test.failing('sequelize bug /players?order[0]={"model":"Team"}&order[0]=name&order[0]=DESC', async (t) => {
const { server, instances } = t.context;
const { player1, player2, player3 } = instances;
const url = '/players?order[0]={"model":"Team"}&order[0]=name&order[0]=DESC';
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
// this is the order we'd expect the names to be in
t.is(result[0].name, player3.name);
t.is(result[1].name, player1.name);
t.is(result[2].name, player2.name);
});
// b/c the above fails, this is a work-around
test('/players?order[0]={"model":"Team"}&order[0]=name&order[0]=DESC&include=team', async (t) => {
const { server, instances } = t.context;
const { player1, player2, player3 } = instances;
const url = '/players?order[0]={"model":"Team"}&order[0]=name&order[0]=DESC&include=team';
const method = 'GET';
const { result, statusCode } = await server.inject({ url, method });
t.is(statusCode, STATUS_OK);
// this is the order we'd expect the names to be in
t.is(result[0].name, player3.name);
t.is(result[1].name, player1.name);
t.is(result[2].name, player2.name);
});
test('invalid column /players?order[0]=invalid', async (t) => {

View File

@ -165,6 +165,8 @@ export const list = ({ server, model, prefix = '/', config }) => {
where, include, limit, offset, order,
});
if (!list.length) return void reply(notFound('Nothing found.'));
reply(list.map((item) => item.toJSON()));
},

View File

@ -3,21 +3,27 @@ import { notImplemented } from 'boom';
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
const getModels = (request) => {
const noGetDb = typeof request.getDb !== 'function';
const noRequestModels = !request.models;
if (noGetDb && noRequestModels) {
return notImplemented('`request.getDb` or `request.models` are not defined.'
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
}
const { models } = noGetDb ? request : request.getDb();
return models;
};
export const parseInclude = request => {
const include = Array.isArray(request.query.include)
? request.query.include
: [request.query.include]
;
const noGetDb = typeof request.getDb !== 'function';
const noRequestModels = !request.models;
if (noGetDb && noRequestModels) {
return notImplemented('`request.getDb` or `request.models` are not defined.'
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
}
const { models } = noGetDb ? request : request.getDb();
const models = getModels(request);
if (models.isBoom) return models;
return include.map(a => {
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
@ -63,24 +69,40 @@ export const parseLimitAndOffset = (request) => {
return out;
};
const parseOrderArray = (order, models) => {
return order.map((requestColumn) => {
if (Array.isArray(requestColumn)) {
return parseOrderArray(requestColumn, models);
}
let column;
try {
column = JSON.parse(requestColumn);
} catch (e) {
column = requestColumn;
}
if (column.model) column.model = models[column.model];
return column;
});
};
export const parseOrder = (request) => {
const { order } = request.query;
if (!order) return null;
const models = getModels(request);
if (models.isBoom) return models;
// transform to an array so sequelize will escape the input for us and
// maintain security. See http://docs.sequelizejs.com/en/latest/docs/querying/#ordering
if (isString(order)) return [order.split(' ')];
const requestOrderColumns = isString(order) ? [order.split(' ')] : order;
for (const key of Object.keys(order)) {
try {
order[key] = JSON.parse(order[key]);
} catch (e) {
//
}
}
const parsedOrder = parseOrderArray(requestOrderColumns, models);
return [order];
return parsedOrder;
};
export const getMethod = (model, association, plural = true, method = 'get') => {

View File

@ -2,7 +2,8 @@ import test from 'ava';
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
test.beforeEach((t) => {
t.context.request = { query: {} };
const models = t.context.models = { User: {} };
t.context.request = { query: {}, models };
});
test('parseLimitAndOffset is a function', (t) => {
@ -62,14 +63,13 @@ test('parseOrder returns order when a string', (t) => {
});
test('parseOrder returns order when json', (t) => {
const { request } = t.context;
const order = [{ model: 'User' }, 'DESC'];
const { request,models } = t.context;
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
request.query.thing = 'hi';
t.deepEqual(
parseOrder(request)
, [order]
, [{ model: models.User }, 'DESC']
);
});

View File

@ -27,6 +27,13 @@ export default (sequelize, DataTypes) => {
name: 'notaname',
},
},
returnsAll: {
where: {
name: {
$ne: 'notaname',
},
},
},
},
});
};

View File

@ -58,11 +58,13 @@ export default (test) => {
const { Player, Team, City } = t.context.sequelize.models;
const city1 = await City.create({ name: 'Healdsburg' });
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
const team2 = await Team.create({ name: 'Footballs', cityId: city1.id });
const player1 = await Player.create({
name: 'Pinot', teamId: team1.id, active: true,
name: 'Cat', teamId: team1.id, active: true,
});
const player2 = await Player.create({ name: 'Syrah', teamId: team1.id });
t.context.instances = { city1, team1, player1, player2 };
const player2 = await Player.create({ name: 'Pinot', teamId: team1.id });
const player3 = await Player.create({ name: 'Syrah', teamId: team2.id });
t.context.instances = { city1, team1, team2, player1, player2, player3 };
});
// kill the server so that we can exit and don't leak memory