Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
facde8d542 | ||
|
0cbc28ef90 | ||
|
a6590a9650 | ||
|
85b52fd5ab | ||
|
5ba9d7d261 | ||
|
07837ef36c | ||
|
25501bbb10 | ||
|
a335471f02 | ||
|
ce26814f74 | ||
|
d1fc6d46e8 | ||
|
4e94c7f825 | ||
|
c289fb2ed4 | ||
|
e1b851f932 | ||
|
34e37217f1 | ||
|
6a80149916 | ||
|
cb6ea51836 | ||
|
5aec1242db | ||
|
8fb3f2e849 | ||
|
11e6ff596c | ||
|
6a2290f064 | ||
|
1daa68e03e | ||
|
01081db7a3 | ||
|
3b962ce4d8 | ||
|
f638680e29 | ||
|
94e9870133 | ||
|
0713f81301 | ||
|
f49e4daf79 | ||
|
087e64607c | ||
|
57f95f8c95 | ||
|
10d108878a |
3
.gitignore
vendored
3
.gitignore
vendored
@ -36,3 +36,6 @@ npm-debug.log
|
|||||||
|
|
||||||
# System
|
# System
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
coverage.lcov
|
||||||
|
.nyc_output
|
||||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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.
|
17
README.md
17
README.md
@ -158,6 +158,7 @@ Team.findAll({order: ['name']})
|
|||||||
```js
|
```js
|
||||||
// returns the teams ordered by the name column, descending
|
// returns the teams ordered by the name column, descending
|
||||||
// GET /teams?order[0]=name&order[0]=DESC
|
// GET /teams?order[0]=name&order[0]=DESC
|
||||||
|
// GET /teams?order=name%20DESC
|
||||||
|
|
||||||
// results in a Sequelize query:
|
// results in a Sequelize query:
|
||||||
Team.findAll({order: [['name', 'DESC']]})
|
Team.findAll({order: [['name', 'DESC']]})
|
||||||
@ -171,6 +172,22 @@ Team.findAll({order: [['name', 'DESC']]})
|
|||||||
Team.findAll({order: [['name'], ['city']]})
|
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
|
## 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.
|
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,9 +1,13 @@
|
|||||||
machine:
|
machine:
|
||||||
node:
|
node:
|
||||||
version: 6.5.0
|
version: 6.9.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
pre:
|
pre:
|
||||||
- npm prune
|
- npm prune
|
||||||
post:
|
post:
|
||||||
- mkdir -p $CIRCLE_TEST_REPORTS/ava
|
- mkdir -p $CIRCLE_TEST_REPORTS/ava
|
||||||
|
|
||||||
|
test:
|
||||||
|
post:
|
||||||
|
- npm run coverage
|
||||||
|
29
package.json
29
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hapi-sequelize-crud",
|
"name": "hapi-sequelize-crud",
|
||||||
"version": "2.7.2",
|
"version": "2.9.3",
|
||||||
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"config": {
|
"config": {
|
||||||
@ -10,10 +10,11 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --tap=${CI-false} src/**/*.test.js | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)",
|
"test": "SCRIPTY_SILENT=true scripty",
|
||||||
"tdd": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --watch src/**/*.test.js",
|
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
|
||||||
"build": "scripty",
|
"tdd": "ava --watch",
|
||||||
"watch": "scripty"
|
"build": "SCRIPTY_SILENT=true scripty",
|
||||||
|
"watch": "SCRIPTY_SILENT=true scripty"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
|
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
|
||||||
@ -33,12 +34,14 @@
|
|||||||
"babel-preset-stage-1": "^6.16.0",
|
"babel-preset-stage-1": "^6.16.0",
|
||||||
"babel-register": "^6.16.3",
|
"babel-register": "^6.16.3",
|
||||||
"bluebird": "^3.4.6",
|
"bluebird": "^3.4.6",
|
||||||
|
"codecov": "^1.0.1",
|
||||||
"eslint": "^3.8.1",
|
"eslint": "^3.8.1",
|
||||||
"eslint-config-pichak": "^1.1.2",
|
"eslint-config-pichak": "^1.1.2",
|
||||||
"eslint-plugin-ava": "^3.1.1",
|
"eslint-plugin-ava": "^3.1.1",
|
||||||
"ghooks": "^1.3.2",
|
"ghooks": "^1.3.2",
|
||||||
"hapi": "^15.2.0",
|
"hapi": "^15.2.0",
|
||||||
"hapi-sequelize": "^3.0.4",
|
"hapi-sequelize": "^3.0.4",
|
||||||
|
"nyc": "^8.3.2",
|
||||||
"portfinder": "^1.0.9",
|
"portfinder": "^1.0.9",
|
||||||
"scripty": "^1.6.0",
|
"scripty": "^1.6.0",
|
||||||
"sequelize": "^3.24.6",
|
"sequelize": "^3.24.6",
|
||||||
@ -54,5 +57,21 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"babel-polyfill": "^6.13.0"
|
"babel-polyfill": "^6.13.0"
|
||||||
|
},
|
||||||
|
"nyc": {
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"ava": {
|
||||||
|
"source": [
|
||||||
|
"src/**/*.js",
|
||||||
|
"!build/**/*"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"**/*.test.js",
|
||||||
|
"!build/**/*"
|
||||||
|
],
|
||||||
|
"require": [
|
||||||
|
"babel-register"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
scripts/test.sh
Executable file
14
scripts/test.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
||||||
|
set -euo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
nyc=./node_modules/.bin/nyc
|
||||||
|
ava=./node_modules/.bin/ava
|
||||||
|
|
||||||
|
if [ ! -z ${CI:-} ]; then
|
||||||
|
$nyc $ava --tap=${CI-false} | tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml
|
||||||
|
else
|
||||||
|
$nyc $ava
|
||||||
|
fi
|
||||||
|
|
44
src/crud-create.integration.test.js
Normal file
44
src/crud-create.integration.test.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
const STATUS_NOT_FOUND = 404;
|
||||||
|
const STATUS_BAD_REQUEST = 400;
|
||||||
|
|
||||||
|
setup(test);
|
||||||
|
|
||||||
|
test('where /player {name: "Chard"}', async (t) => {
|
||||||
|
const { server, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const url = '/player';
|
||||||
|
const method = 'POST';
|
||||||
|
const payload = { name: 'Chard' };
|
||||||
|
|
||||||
|
const notPresentPlayer = await Player.findOne({ where: payload });
|
||||||
|
t.falsy(notPresentPlayer);
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method, payload });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.truthy(result.id);
|
||||||
|
t.is(result.name, payload.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not found /notamodel {name: "Chard"}', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/notamodel';
|
||||||
|
const method = 'POST';
|
||||||
|
const payload = { name: 'Chard' };
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method, payload });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('no payload /player/1', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/player';
|
||||||
|
const method = 'POST';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_BAD_REQUEST);
|
||||||
|
});
|
177
src/crud-destroy.integration.test.js
Normal file
177
src/crud-destroy.integration.test.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
const STATUS_NOT_FOUND = 404;
|
||||||
|
const STATUS_BAD_REQUEST = 400;
|
||||||
|
|
||||||
|
setup(test);
|
||||||
|
|
||||||
|
test('destroy where /player?name=Baseball', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
const url = `/player?name=${player1.name}`;
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayer = await Player.findById(player1.id);
|
||||||
|
t.truthy(presentPlayer);
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, player1.id);
|
||||||
|
|
||||||
|
const deletedPlayer = await Player.findById(player1.id);
|
||||||
|
t.falsy(deletedPlayer);
|
||||||
|
const stillTherePlayer = await Player.findById(player2.id);
|
||||||
|
t.truthy(stillTherePlayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyAll where /players?name=Baseball', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
const url = `/players?name=${player1.name}`;
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayer = await Player.findById(player1.id);
|
||||||
|
t.truthy(presentPlayer);
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, player1.id);
|
||||||
|
|
||||||
|
const deletedPlayer = await Player.findById(player1.id);
|
||||||
|
t.falsy(deletedPlayer);
|
||||||
|
const stillTherePlayer = await Player.findById(player2.id);
|
||||||
|
t.truthy(stillTherePlayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyAll /players', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
const url = '/players';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
const resultPlayerIds = result.map(({ id }) => id);
|
||||||
|
t.truthy(resultPlayerIds.includes(player1.id));
|
||||||
|
t.truthy(resultPlayerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const deletedPlayers = await Player.findAll();
|
||||||
|
t.is(deletedPlayers.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy not found /player/10', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/player/10';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
|
||||||
|
const nonDeletedPlayers = await Player.findAll();
|
||||||
|
t.is(nonDeletedPlayers.length, presentPlayers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyAll not found /players?name=no', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/players?name=no';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
|
||||||
|
const nonDeletedPlayers = await Player.findAll();
|
||||||
|
t.is(nonDeletedPlayers.length, presentPlayers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not found /notamodel', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/notamodel';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyScope /players/returnsOne', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/players/returnsOne';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, player1.id);
|
||||||
|
|
||||||
|
const nonDeletedPlayers = await Player.findAll();
|
||||||
|
t.is(nonDeletedPlayers.length, presentPlayers.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyScope /players/returnsNone', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/players/returnsNone';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
|
||||||
|
const nonDeletedPlayers = await Player.findAll();
|
||||||
|
const nonDeletedPlayerIds = nonDeletedPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(nonDeletedPlayerIds.includes(player1.id));
|
||||||
|
t.truthy(nonDeletedPlayerIds.includes(player2.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroyScope invalid scope /players/invalid', async (t) => {
|
||||||
|
const { server, instances, sequelize: { models: { Player } } } = t.context;
|
||||||
|
const { player1, player2 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/players/invalid';
|
||||||
|
const method = 'DELETE';
|
||||||
|
|
||||||
|
const presentPlayers = await Player.findAll();
|
||||||
|
const playerIds = presentPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_BAD_REQUEST);
|
||||||
|
|
||||||
|
const nonDeletedPlayers = await Player.findAll();
|
||||||
|
const nonDeletedPlayerIds = nonDeletedPlayers.map(({ id }) => id);
|
||||||
|
t.truthy(nonDeletedPlayerIds.includes(player1.id));
|
||||||
|
t.truthy(nonDeletedPlayerIds.includes(player2.id));
|
||||||
|
});
|
@ -2,6 +2,8 @@ import test from 'ava';
|
|||||||
import 'sinon-bluebird';
|
import 'sinon-bluebird';
|
||||||
import setup from '../test/integration-setup.js';
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
|
||||||
setup(test);
|
setup(test);
|
||||||
|
|
||||||
test('belongsTo /team?include=city', async (t) => {
|
test('belongsTo /team?include=city', async (t) => {
|
||||||
@ -9,8 +11,8 @@ test('belongsTo /team?include=city', async (t) => {
|
|||||||
const { team1, city1 } = instances;
|
const { team1, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=city`;
|
const path = `/team/${team1.id}?include=city`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
t.is(result.City.id, city1.id);
|
t.is(result.City.id, city1.id);
|
||||||
});
|
});
|
||||||
@ -20,8 +22,8 @@ test('belongsTo /team?include=cities', async (t) => {
|
|||||||
const { team1, city1 } = instances;
|
const { team1, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=cities`;
|
const path = `/team/${team1.id}?include=cities`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
t.is(result.City.id, city1.id);
|
t.is(result.City.id, city1.id);
|
||||||
});
|
});
|
||||||
@ -31,8 +33,8 @@ test('hasMany /team?include=player', async (t) => {
|
|||||||
const { team1, player1, player2 } = instances;
|
const { team1, player1, player2 } = instances;
|
||||||
const path = `/team/${team1.id}?include=player`;
|
const path = `/team/${team1.id}?include=player`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
|
|
||||||
const playerIds = result.Players.map(({ id }) => id);
|
const playerIds = result.Players.map(({ id }) => id);
|
||||||
@ -45,8 +47,8 @@ test('hasMany /team?include=players', async (t) => {
|
|||||||
const { team1, player1, player2 } = instances;
|
const { team1, player1, player2 } = instances;
|
||||||
const path = `/team/${team1.id}?include=players`;
|
const path = `/team/${team1.id}?include=players`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
|
|
||||||
const playerIds = result.Players.map(({ id }) => id);
|
const playerIds = result.Players.map(({ id }) => id);
|
||||||
@ -59,8 +61,8 @@ test('multiple includes /team?include=players&include=city', async (t) => {
|
|||||||
const { team1, player1, player2, city1 } = instances;
|
const { team1, player1, player2, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=players&include=city`;
|
const path = `/team/${team1.id}?include=players&include=city`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
|
|
||||||
const playerIds = result.Players.map(({ id }) => id);
|
const playerIds = result.Players.map(({ id }) => id);
|
||||||
@ -74,8 +76,8 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => {
|
|||||||
const { team1, player1, player2, city1 } = instances;
|
const { team1, player1, player2, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include[]=players&include[]=city`;
|
const path = `/team/${team1.id}?include[]=players&include[]=city`;
|
||||||
|
|
||||||
const { result, response } = await server.inject(path);
|
const { result, statusCode } = await server.inject(path);
|
||||||
t.falsy(response instanceof Error);
|
t.is(statusCode, STATUS_OK);
|
||||||
t.is(result.id, team1.id);
|
t.is(result.id, team1.id);
|
||||||
|
|
||||||
const playerIds = result.Players.map(({ id }) => id);
|
const playerIds = result.Players.map(({ id }) => id);
|
||||||
|
94
src/crud-list-limit-and-offset.integration.test.js
Normal file
94
src/crud-list-limit-and-offset.integration.test.js
Normal 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');
|
||||||
|
});
|
141
src/crud-list-order.integration.test.js
Normal file
141
src/crud-list-order.integration.test.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
const STATUS_BAD_QUERY = 502;
|
||||||
|
|
||||||
|
setup(test);
|
||||||
|
|
||||||
|
test('/players?order=name', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { player1, player2, player3 } = instances;
|
||||||
|
const url = '/players?order=name';
|
||||||
|
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, 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 url = '/players?order=name%20ASC';
|
||||||
|
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, 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 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('/players?order[]=name', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { player1, player2, player3 } = instances;
|
||||||
|
const url = '/players?order[]=name';
|
||||||
|
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, 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 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[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) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/players?order[]=invalid';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { statusCode, result } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_BAD_QUERY);
|
||||||
|
t.truthy(result.message.includes('invalid'));
|
||||||
|
});
|
40
src/crud-scope.integration.test.js
Normal file
40
src/crud-scope.integration.test.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
const STATUS_NOT_FOUND = 404;
|
||||||
|
const STATUS_BAD_REQUEST = 400;
|
||||||
|
|
||||||
|
setup(test);
|
||||||
|
|
||||||
|
test('/players/returnsOne', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { player1 } = instances;
|
||||||
|
const url = '/players/returnsOne';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.length, 1);
|
||||||
|
t.truthy(result[0].id, player1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('/players/returnsNone', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/players/returnsNone';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid scope /players/invalid', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/players/invalid';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_BAD_REQUEST);
|
||||||
|
});
|
54
src/crud-update.integration.test.js
Normal file
54
src/crud-update.integration.test.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import test from 'ava';
|
||||||
|
import 'sinon-bluebird';
|
||||||
|
import setup from '../test/integration-setup.js';
|
||||||
|
|
||||||
|
const STATUS_OK = 200;
|
||||||
|
const STATUS_NOT_FOUND = 404;
|
||||||
|
const STATUS_BAD_REQUEST = 400;
|
||||||
|
|
||||||
|
setup(test);
|
||||||
|
|
||||||
|
test('where /player/1 {name: "Chard"}', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { player1 } = instances;
|
||||||
|
const url = `/player/${player1.id}`;
|
||||||
|
const method = 'PUT';
|
||||||
|
const payload = { name: 'Chard' };
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method, payload });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, player1.id);
|
||||||
|
t.is(result.name, payload.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not found /player/10 {name: "Chard"}', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const url = '/player/10';
|
||||||
|
const method = 'PUT';
|
||||||
|
const payload = { name: 'Chard' };
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method, payload });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('no payload /player/1', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { player1 } = instances;
|
||||||
|
const url = `/player/${player1.id}`;
|
||||||
|
const method = 'PUT';
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_BAD_REQUEST);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not found /notamodel {name: "Chard"}', async (t) => {
|
||||||
|
const { server } = t.context;
|
||||||
|
const url = '/notamodel';
|
||||||
|
const method = 'PUT';
|
||||||
|
const payload = { name: 'Chard' };
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject({ url, method, payload });
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
53
src/crud-where.integration.test.js
Normal file
53
src/crud-where.integration.test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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('single result /team?name=Baseball', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { team1 } = instances;
|
||||||
|
const path = `/team?name=${team1.name}`;
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject(path);
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, team1.id);
|
||||||
|
t.is(result.name, team1.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no results /team?name=Baseball&id=2', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { team1 } = instances;
|
||||||
|
// this doesn't exist in our fixtures
|
||||||
|
const path = `/team?name=${team1.name}&id=2`;
|
||||||
|
|
||||||
|
const { statusCode } = await server.inject(path);
|
||||||
|
t.is(statusCode, STATUS_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('single result from list query /teams?name=Baseball', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { team1 } = instances;
|
||||||
|
const path = `/team?name=${team1.name}`;
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject(path);
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, team1.id);
|
||||||
|
t.is(result.name, team1.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple results from list query /players?teamId=1', async (t) => {
|
||||||
|
const { server, instances } = t.context;
|
||||||
|
const { team1, player1, player2 } = instances;
|
||||||
|
const path = `/players?teamId=${team1.id}`;
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject(path);
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
const playerIds = result.map(({ id }) => id);
|
||||||
|
t.truthy(playerIds.includes(player1.id));
|
||||||
|
t.truthy(playerIds.includes(player2.id));
|
||||||
|
});
|
||||||
|
|
24
src/crud.js
24
src/crud.js
@ -165,6 +165,8 @@ export const list = ({ server, model, prefix = '/', config }) => {
|
|||||||
where, include, limit, offset, order,
|
where, include, limit, offset, order,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!list.length) return void reply(notFound('Nothing found.'));
|
||||||
|
|
||||||
reply(list.map((item) => item.toJSON()));
|
reply(list.map((item) => item.toJSON()));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -214,6 +216,8 @@ export const scope = ({ server, model, prefix = '/', config }) => {
|
|||||||
include, where, limit, offset, order,
|
include, where, limit, offset, order,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!list.length) return void reply(notFound('Nothing found.'));
|
||||||
|
|
||||||
reply(list.map((item) => item.toJSON()));
|
reply(list.map((item) => item.toJSON()));
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
@ -244,10 +248,18 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
|
|||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
if (request.params.id) where[model.primaryKeyField] = request.params.id;
|
const { id } = request.params;
|
||||||
|
if (id) where[model.primaryKeyField] = id;
|
||||||
|
|
||||||
const list = await model.findAll({ where });
|
const list = await model.findAll({ where });
|
||||||
|
|
||||||
|
if (!list.length) {
|
||||||
|
return void reply(id
|
||||||
|
? notFound(`${id} not found.`)
|
||||||
|
: notFound('Nothing found.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
|
|
||||||
const listAsJSON = list.map((item) => item.toJSON());
|
const listAsJSON = list.map((item) => item.toJSON());
|
||||||
@ -266,9 +278,17 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
|||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
const list = await model.findAll({ where });
|
const list = await model.findAll({ where });
|
||||||
|
|
||||||
|
if (!list.length) {
|
||||||
|
return void reply(id
|
||||||
|
? notFound(`${id} not found.`)
|
||||||
|
: notFound('Nothing found.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
|
|
||||||
const listAsJSON = list.map((item) => item.toJSON());
|
const listAsJSON = list.map((item) => item.toJSON());
|
||||||
@ -293,6 +313,8 @@ export const destroyScope = ({ server, model, prefix = '/', config }) => {
|
|||||||
|
|
||||||
const list = await model.scope(request.params.scope).findAll({ include, where });
|
const list = await model.scope(request.params.scope).findAll({ include, where });
|
||||||
|
|
||||||
|
if (!list.length) return void reply(notFound('Nothing found.'));
|
||||||
|
|
||||||
await Promise.all(list.map(instance => instance.destroy()));
|
await Promise.all(list.map(instance => instance.destroy()));
|
||||||
|
|
||||||
const listAsJSON = list.map((item) => item.toJSON());
|
const listAsJSON = list.map((item) => item.toJSON());
|
||||||
|
@ -225,7 +225,7 @@ test('crud#list handler with order', async (t) => {
|
|||||||
|
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
findAllArgs.order,
|
findAllArgs.order,
|
||||||
[request.query.order],
|
[[request.query.order]],
|
||||||
'queries with the order as an array b/c that\'s what sequelize wants'
|
'queries with the order as an array b/c that\'s what sequelize wants'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -144,7 +144,7 @@ export default ({
|
|||||||
.keys({
|
.keys({
|
||||||
limit: joi.number().min(0).integer(),
|
limit: joi.number().min(0).integer(),
|
||||||
offset: joi.number().min(0).integer(),
|
offset: joi.number().min(0).integer(),
|
||||||
order: joi.array(),
|
order: [joi.array(), joi.string()],
|
||||||
}),
|
}),
|
||||||
get(methodConfig, 'validate.query')
|
get(methodConfig, 'validate.query')
|
||||||
);
|
);
|
||||||
|
@ -38,7 +38,6 @@ const register = (server, options = {}, next) => {
|
|||||||
// Join tables
|
// Join tables
|
||||||
if (model.options.name.singular !== model.name) continue;
|
if (model.options.name.singular !== model.name) continue;
|
||||||
|
|
||||||
|
|
||||||
for (const key of Object.keys(model.associations)) {
|
for (const key of Object.keys(model.associations)) {
|
||||||
const association = model.associations[key];
|
const association = model.associations[key];
|
||||||
const { source, target } = association;
|
const { source, target } = association;
|
||||||
@ -95,11 +94,15 @@ const register = (server, options = {}, next) => {
|
|||||||
|
|
||||||
// build the methods for each model now that we've defined all the
|
// build the methods for each model now that we've defined all the
|
||||||
// associations
|
// associations
|
||||||
Object.keys(models).forEach((modelName) => {
|
Object.keys(models).filter((modelName) => {
|
||||||
|
const model = models[modelName];
|
||||||
|
return model.options.name.singular === model.name;
|
||||||
|
}).forEach((modelName) => {
|
||||||
const model = models[modelName];
|
const model = models[modelName];
|
||||||
crud(server, model, options);
|
crud(server, model, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
58
src/utils.js
58
src/utils.js
@ -3,21 +3,27 @@ import { notImplemented } from 'boom';
|
|||||||
|
|
||||||
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
|
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 => {
|
export const parseInclude = request => {
|
||||||
const include = Array.isArray(request.query.include)
|
const include = Array.isArray(request.query.include)
|
||||||
? request.query.include
|
? request.query.include
|
||||||
: [request.query.include]
|
: [request.query.include]
|
||||||
;
|
;
|
||||||
|
|
||||||
const noGetDb = typeof request.getDb !== 'function';
|
const models = getModels(request);
|
||||||
const noRequestModels = !request.models;
|
if (models.isBoom) return 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 => {
|
return include.map(a => {
|
||||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
||||||
@ -63,24 +69,40 @@ export const parseLimitAndOffset = (request) => {
|
|||||||
return out;
|
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) => {
|
export const parseOrder = (request) => {
|
||||||
const { order } = request.query;
|
const { order } = request.query;
|
||||||
|
|
||||||
if (!order) return null;
|
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
|
// 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
|
// 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)) {
|
const parsedOrder = parseOrderArray(requestOrderColumns, models);
|
||||||
try {
|
|
||||||
order[key] = JSON.parse(order[key]);
|
|
||||||
} catch (e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return order;
|
return parsedOrder;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMethod = (model, association, plural = true, method = 'get') => {
|
export const getMethod = (model, association, plural = true, method = 'get') => {
|
||||||
|
@ -2,7 +2,8 @@ import test from 'ava';
|
|||||||
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
|
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
|
||||||
|
|
||||||
test.beforeEach((t) => {
|
test.beforeEach((t) => {
|
||||||
t.context.request = { query: {} };
|
const models = t.context.models = { User: {} };
|
||||||
|
t.context.request = { query: {}, models };
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parseLimitAndOffset is a function', (t) => {
|
test('parseLimitAndOffset is a function', (t) => {
|
||||||
@ -57,19 +58,18 @@ test('parseOrder returns order when a string', (t) => {
|
|||||||
|
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
parseOrder(request)
|
parseOrder(request)
|
||||||
, [order]
|
, [[order]]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parseOrder returns order when json', (t) => {
|
test('parseOrder returns order when json', (t) => {
|
||||||
const { request } = t.context;
|
const { request,models } = t.context;
|
||||||
const order = [{ model: 'User' }, 'DESC'];
|
|
||||||
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
|
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
|
||||||
request.query.thing = 'hi';
|
request.query.thing = 'hi';
|
||||||
|
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
parseOrder(request)
|
parseOrder(request)
|
||||||
, order
|
, [{ model: models.User }, 'DESC']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
20
test/fixtures/models/player.js
vendored
20
test/fixtures/models/player.js
vendored
@ -7,6 +7,7 @@ export default (sequelize, DataTypes) => {
|
|||||||
},
|
},
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
teamId: DataTypes.INTEGER,
|
teamId: DataTypes.INTEGER,
|
||||||
|
active: DataTypes.BOOLEAN,
|
||||||
}, {
|
}, {
|
||||||
classMethods: {
|
classMethods: {
|
||||||
associate: (models) => {
|
associate: (models) => {
|
||||||
@ -15,5 +16,24 @@ export default (sequelize, DataTypes) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
scopes: {
|
||||||
|
returnsOne: {
|
||||||
|
where: {
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returnsNone: {
|
||||||
|
where: {
|
||||||
|
name: 'notaname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returnsAll: {
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
$ne: 'notaname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -58,9 +58,13 @@ export default (test) => {
|
|||||||
const { Player, Team, City } = t.context.sequelize.models;
|
const { Player, Team, City } = t.context.sequelize.models;
|
||||||
const city1 = await City.create({ name: 'Healdsburg' });
|
const city1 = await City.create({ name: 'Healdsburg' });
|
||||||
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
|
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
|
||||||
const player1 = await Player.create({ name: 'Pinot', teamId: team1.id });
|
const team2 = await Team.create({ name: 'Footballs', cityId: city1.id });
|
||||||
const player2 = await Player.create({ name: 'Syrah', teamId: team1.id });
|
const player1 = await Player.create({
|
||||||
t.context.instances = { city1, team1, player1, player2 };
|
name: 'Cat', 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 };
|
||||||
});
|
});
|
||||||
|
|
||||||
// kill the server so that we can exit and don't leak memory
|
// kill the server so that we can exit and don't leak memory
|
||||||
|
Loading…
x
Reference in New Issue
Block a user