Compare commits
No commits in common. "master" and "v2.8.0" have entirely different histories.
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 The hapi-sequelize-crud Authors
|
||||
|
||||
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.
|
16
README.md
16
README.md
@ -172,22 +172,6 @@ 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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hapi-sequelize-crud",
|
||||
"version": "2.9.3",
|
||||
"version": "2.8.0",
|
||||
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
||||
"main": "build/index.js",
|
||||
"config": {
|
||||
|
@ -1,94 +0,0 @@
|
||||
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');
|
||||
});
|
@ -9,7 +9,7 @@ setup(test);
|
||||
|
||||
test('/players?order=name', async (t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { player1, player2, player3 } = instances;
|
||||
const { player1, player2 } = instances;
|
||||
const url = '/players?order=name';
|
||||
const method = 'GET';
|
||||
|
||||
@ -18,12 +18,11 @@ 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, player3 } = instances;
|
||||
const { player1, player2 } = instances;
|
||||
const url = '/players?order=name%20ASC';
|
||||
const method = 'GET';
|
||||
|
||||
@ -32,26 +31,24 @@ 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, player3 } = instances;
|
||||
const { player1, player2 } = 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, player3.name);
|
||||
t.is(result[1].name, player2.name);
|
||||
t.is(result[2].name, player1.name);
|
||||
t.is(result[0].name, player2.name);
|
||||
t.is(result[1].name, player1.name);
|
||||
});
|
||||
|
||||
test('/players?order[]=name', async (t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { player1, player2, player3 } = instances;
|
||||
const { player1, player2 } = instances;
|
||||
const url = '/players?order[]=name';
|
||||
const method = 'GET';
|
||||
|
||||
@ -60,74 +57,19 @@ 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, player3 } = instances;
|
||||
const { player1, player2 } = 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, 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[0].name, player2.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) => {
|
||||
|
@ -165,8 +165,6 @@ 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()));
|
||||
},
|
||||
|
||||
|
@ -38,6 +38,7 @@ const register = (server, options = {}, next) => {
|
||||
// Join tables
|
||||
if (model.options.name.singular !== model.name) continue;
|
||||
|
||||
|
||||
for (const key of Object.keys(model.associations)) {
|
||||
const association = model.associations[key];
|
||||
const { source, target } = association;
|
||||
@ -94,15 +95,11 @@ const register = (server, options = {}, next) => {
|
||||
|
||||
// build the methods for each model now that we've defined all the
|
||||
// associations
|
||||
Object.keys(models).filter((modelName) => {
|
||||
const model = models[modelName];
|
||||
return model.options.name.singular === model.name;
|
||||
}).forEach((modelName) => {
|
||||
Object.keys(models).forEach((modelName) => {
|
||||
const model = models[modelName];
|
||||
crud(server, model, options);
|
||||
});
|
||||
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
58
src/utils.js
58
src/utils.js
@ -3,27 +3,21 @@ 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 models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
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 include.map(a => {
|
||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
||||
@ -69,40 +63,24 @@ 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
|
||||
const requestOrderColumns = isString(order) ? [order.split(' ')] : order;
|
||||
if (isString(order)) return [order.split(' ')];
|
||||
|
||||
const parsedOrder = parseOrderArray(requestOrderColumns, models);
|
||||
for (const key of Object.keys(order)) {
|
||||
try {
|
||||
order[key] = JSON.parse(order[key]);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
return parsedOrder;
|
||||
return [order];
|
||||
};
|
||||
|
||||
export const getMethod = (model, association, plural = true, method = 'get') => {
|
||||
|
@ -2,8 +2,7 @@ import test from 'ava';
|
||||
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
|
||||
|
||||
test.beforeEach((t) => {
|
||||
const models = t.context.models = { User: {} };
|
||||
t.context.request = { query: {}, models };
|
||||
t.context.request = { query: {} };
|
||||
});
|
||||
|
||||
test('parseLimitAndOffset is a function', (t) => {
|
||||
@ -63,13 +62,14 @@ test('parseOrder returns order when a string', (t) => {
|
||||
});
|
||||
|
||||
test('parseOrder returns order when json', (t) => {
|
||||
const { request,models } = t.context;
|
||||
const { request } = t.context;
|
||||
const order = [{ model: 'User' }, 'DESC'];
|
||||
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
|
||||
request.query.thing = 'hi';
|
||||
|
||||
t.deepEqual(
|
||||
parseOrder(request)
|
||||
, [{ model: models.User }, 'DESC']
|
||||
, [order]
|
||||
);
|
||||
});
|
||||
|
||||
|
7
test/fixtures/models/player.js
vendored
7
test/fixtures/models/player.js
vendored
@ -27,13 +27,6 @@ export default (sequelize, DataTypes) => {
|
||||
name: 'notaname',
|
||||
},
|
||||
},
|
||||
returnsAll: {
|
||||
where: {
|
||||
name: {
|
||||
$ne: 'notaname',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -58,13 +58,11 @@ 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: 'Cat', teamId: team1.id, active: true,
|
||||
name: 'Pinot', teamId: team1.id, active: true,
|
||||
});
|
||||
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 };
|
||||
const player2 = await Player.create({ name: 'Syrah', teamId: team1.id });
|
||||
t.context.instances = { city1, team1, player1, player2 };
|
||||
});
|
||||
|
||||
// kill the server so that we can exit and don't leak memory
|
||||
|
Loading…
x
Reference in New Issue
Block a user