25 Commits

Author SHA1 Message Date
34e37217f1 2.8.0 2016-10-28 11:28:23 -07:00
6a80149916 Merge pull request #31 from mdibaiee/code-coverage
Add more integration tests
2016-10-28 11:28:06 -07:00
cb6ea51836 Test add integration tests for scope 2016-10-28 11:22:52 -07:00
5aec1242db Test add integration tests for ordering lists 2016-10-28 11:22:38 -07:00
8fb3f2e849 Fix(crud) actually enable multiple orders
This was supposed to work, but adding integration tests I realized it
didn't. #oops
2016-10-28 11:22:05 -07:00
11e6ff596c Fix(crud) scope now 404s on no results 2016-10-28 11:20:59 -07:00
6a2290f064 Docs add docs for additional order option 2016-10-28 11:20:12 -07:00
1daa68e03e Fix(crud) destroyScope sends 404 when not found 2016-10-27 21:03:57 -07:00
01081db7a3 Test add destroyScope tests 2016-10-27 21:03:32 -07:00
3b962ce4d8 2.7.3 2016-10-27 13:20:46 -07:00
f638680e29 Merge pull request #30 from mdibaiee/code-coverage
Add integration tests
2016-10-27 13:20:18 -07:00
94e9870133 Fix(crud) 404 errors for destroy and destroyAll 2016-10-27 12:33:31 -07:00
0713f81301 Test add CRUD tests
boosting our test coverage
2016-10-27 12:33:02 -07:00
f49e4daf79 Test fix error checking in include tests #oops 2016-10-27 12:32:36 -07:00
087e64607c Merge pull request #29 from mdibaiee/code-coverage
Add code coverage
2016-10-26 18:14:37 -07:00
57f95f8c95 Test(CI) setup code coverage
Also moves ava config to package.json
2016-10-26 18:10:32 -07:00
10d108878a Chore(deps) install codecov and nyc 2016-10-26 17:24:59 -07:00
eebf7b91f0 2.7.2 2016-10-26 14:09:42 -07:00
a45a3ab317 Merge pull request #27 from mdibaiee/add-integration-tests
Add integration tests
2016-10-26 14:08:38 -07:00
7a8cd26dc8 Test add integration tests for ?include 2016-10-26 13:27:13 -07:00
80d0a74c82 Test add integration tests for route creation 2016-10-26 13:26:56 -07:00
863aa1d98b Test add fixtures and integration setup 2016-10-26 13:26:39 -07:00
90f72cb07a Fix(crud) models w/o associations validation 2016-10-26 13:26:15 -07:00
d3976fa44b Fix prefix should default to /, not ''
b/c `route` isn't a valid Hapi route, but `/route` is.
2016-10-26 13:25:27 -07:00
966b35164f Chore(deps) install dev deps for integration tests 2016-10-26 13:24:27 -07:00
23 changed files with 794 additions and 16 deletions

3
.gitignore vendored
View File

@ -36,3 +36,6 @@ npm-debug.log
# System
.DS_Store
coverage.lcov
.nyc_output

View File

@ -158,6 +158,7 @@ Team.findAll({order: ['name']})
```js
// returns the teams ordered by the name column, descending
// GET /teams?order[0]=name&order[0]=DESC
// GET /teams?order=name%20DESC
// results in a Sequelize query:
Team.findAll({order: [['name', 'DESC']]})

View File

@ -1,9 +1,13 @@
machine:
node:
version: 6.5.0
version: 6.9.0
dependencies:
pre:
- npm prune
post:
- mkdir -p $CIRCLE_TEST_REPORTS/ava
test:
post:
- npm run coverage

View File

@ -1,6 +1,6 @@
{
"name": "hapi-sequelize-crud",
"version": "2.7.1",
"version": "2.8.0",
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js",
"config": {
@ -10,10 +10,11 @@
},
"scripts": {
"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;)",
"tdd": "ava --require babel-register --source='src/**/*.js' --source='!build/**/*' --watch src/**/*.test.js",
"build": "scripty",
"watch": "scripty"
"test": "SCRIPTY_SILENT=true scripty",
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"tdd": "ava --watch",
"build": "SCRIPTY_SILENT=true scripty",
"watch": "SCRIPTY_SILENT=true scripty"
},
"repository": {
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
@ -32,13 +33,21 @@
"babel-plugin-transform-es2015-modules-commonjs": "^6.16.0",
"babel-preset-stage-1": "^6.16.0",
"babel-register": "^6.16.3",
"bluebird": "^3.4.6",
"codecov": "^1.0.1",
"eslint": "^3.8.1",
"eslint-config-pichak": "^1.1.2",
"eslint-plugin-ava": "^3.1.1",
"ghooks": "^1.3.2",
"hapi": "^15.2.0",
"hapi-sequelize": "^3.0.4",
"nyc": "^8.3.2",
"portfinder": "^1.0.9",
"scripty": "^1.6.0",
"sequelize": "^3.24.6",
"sinon": "^1.17.6",
"sinon-bluebird": "^3.1.0",
"sqlite3": "^3.1.7",
"tap-xunit": "^1.4.0"
},
"dependencies": {
@ -48,5 +57,21 @@
},
"optionalDependencies": {
"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
View 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

View 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);
});

View 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));
});

View File

@ -0,0 +1,87 @@
import test from 'ava';
import 'sinon-bluebird';
import setup from '../test/integration-setup.js';
const STATUS_OK = 200;
setup(test);
test('belongsTo /team?include=city', async (t) => {
const { server, instances } = t.context;
const { team1, city1 } = instances;
const path = `/team/${team1.id}?include=city`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
t.is(result.City.id, city1.id);
});
test('belongsTo /team?include=cities', async (t) => {
const { server, instances } = t.context;
const { team1, city1 } = instances;
const path = `/team/${team1.id}?include=cities`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
t.is(result.City.id, city1.id);
});
test('hasMany /team?include=player', async (t) => {
const { server, instances } = t.context;
const { team1, player1, player2 } = instances;
const path = `/team/${team1.id}?include=player`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
const playerIds = result.Players.map(({ id }) => id);
t.truthy(playerIds.includes(player1.id));
t.truthy(playerIds.includes(player2.id));
});
test('hasMany /team?include=players', async (t) => {
const { server, instances } = t.context;
const { team1, player1, player2 } = instances;
const path = `/team/${team1.id}?include=players`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
const playerIds = result.Players.map(({ id }) => id);
t.truthy(playerIds.includes(player1.id));
t.truthy(playerIds.includes(player2.id));
});
test('multiple includes /team?include=players&include=city', async (t) => {
const { server, instances } = t.context;
const { team1, player1, player2, city1 } = instances;
const path = `/team/${team1.id}?include=players&include=city`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
const playerIds = result.Players.map(({ id }) => id);
t.truthy(playerIds.includes(player1.id));
t.truthy(playerIds.includes(player2.id));
t.is(result.City.id, city1.id);
});
test('multiple includes /team?include[]=players&include[]=city', async (t) => {
const { server, instances } = t.context;
const { team1, player1, player2, city1 } = instances;
const path = `/team/${team1.id}?include[]=players&include[]=city`;
const { result, statusCode } = await server.inject(path);
t.is(statusCode, STATUS_OK);
t.is(result.id, team1.id);
const playerIds = result.Players.map(({ id }) => id);
t.truthy(playerIds.includes(player1.id));
t.truthy(playerIds.includes(player2.id));
t.is(result.City.id, city1.id);
});

View File

@ -0,0 +1,83 @@
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 } = 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);
});
test('/players?order=name%20ASC', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = 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);
});
test('/players?order=name%20DESC', async (t) => {
const { server, instances } = t.context;
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, player2.name);
t.is(result[1].name, player1.name);
});
test('/players?order[]=name', async (t) => {
const { server, instances } = t.context;
const { player1, player2 } = 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);
});
test('/players?order[0]=name&order[0]=DESC', async (t) => {
const { server, instances } = t.context;
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, player2.name);
t.is(result[1].name, player1.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'));
});

View File

@ -0,0 +1,26 @@
import test from 'ava';
import 'sinon-bluebird';
import setup from '../test/integration-setup.js';
const { modelNames } = setup(test);
const confirmRoute = (t, { path, method }) => {
const { server } = t.context;
// there's only one connection, so just get the first table
const routes = server.table()[0].table;
t.truthy(routes.find((route) => {
return route.path = path
&& route.method === method;
}));
};
modelNames.forEach(({ singular, plural }) => {
test('get', confirmRoute, { path: `/${singular}/{id}`, method: 'get' });
test('list', confirmRoute, { path: `/${plural}/{id}`, method: 'get' });
test('scope', confirmRoute, { path: `/${plural}/{scope}`, method: 'get' });
test('create', confirmRoute, { path: `/${singular}`, method: 'post' });
test('destroy', confirmRoute, { path: `/${plural}`, method: 'delete' });
test('destroyScope', confirmRoute, { path: `/${plural}/{scope}`, method: 'delete' });
test('update', confirmRoute, { path: `/${singular}/{id}`, method: 'put' });
});

View 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);
});

View 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);
});

View 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));
});

View File

@ -63,14 +63,17 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
const { _singular, _plural, _Singular, _Plural } = target;
return [_singular, _plural, _Singular, _Plural];
}),
];
].filter(Boolean);
const attributeValidation = modelAttributes.reduce((params, attribute) => {
// TODO: use joi-sequelize
params[attribute] = joi.any();
return params;
}, {});
const validAssociations = joi.string().valid(...modelAssociations);
const validAssociations = modelAssociations.length
? joi.string().valid(...modelAssociations)
: joi.valid(null);
const associationValidation = {
include: [joi.array().items(validAssociations), validAssociations],
};
@ -211,6 +214,8 @@ export const scope = ({ server, model, prefix = '/', config }) => {
include, where, limit, offset, order,
});
if (!list.length) return void reply(notFound('Nothing found.'));
reply(list.map((item) => item.toJSON()));
},
config,
@ -241,10 +246,18 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
@error
async handler(request, reply) {
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 });
if (!list.length) {
return void reply(id
? notFound(`${id} not found.`)
: notFound('Nothing found.')
);
}
await Promise.all(list.map(instance => instance.destroy()));
const listAsJSON = list.map((item) => item.toJSON());
@ -263,9 +276,17 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
@error
async handler(request, reply) {
const where = parseWhere(request);
const { id } = request.params;
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()));
const listAsJSON = list.map((item) => item.toJSON());
@ -290,6 +311,8 @@ export const destroyScope = ({ server, model, prefix = '/', config }) => {
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()));
const listAsJSON = list.map((item) => item.toJSON());

View File

@ -225,7 +225,7 @@ test('crud#list handler with order', async (t) => {
t.deepEqual(
findAllArgs.order,
[request.query.order],
[[request.query.order]],
'queries with the order as an array b/c that\'s what sequelize wants'
);
});

View File

@ -144,7 +144,7 @@ export default ({
.keys({
limit: joi.number().min(0).integer(),
offset: joi.number().min(0).integer(),
order: joi.array(),
order: [joi.array(), joi.string()],
}),
get(methodConfig, 'validate.query')
);

View File

@ -7,7 +7,7 @@ import url from 'url';
import qs from 'qs';
const register = (server, options = {}, next) => {
options.prefix = options.prefix || '';
options.prefix = options.prefix || '/';
options.name = options.name || 'db';
const db = server.plugins['hapi-sequelize'][options.name];

View File

@ -70,7 +70,7 @@ export const parseOrder = (request) => {
// 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(' ');
if (isString(order)) return [order.split(' ')];
for (const key of Object.keys(order)) {
try {
@ -80,7 +80,7 @@ export const parseOrder = (request) => {
}
}
return order;
return [order];
};
export const getMethod = (model, association, plural = true, method = 'get') => {

View File

@ -57,7 +57,7 @@ test('parseOrder returns order when a string', (t) => {
t.deepEqual(
parseOrder(request)
, [order]
, [[order]]
);
});
@ -69,7 +69,7 @@ test('parseOrder returns order when json', (t) => {
t.deepEqual(
parseOrder(request)
, order
, [order]
);
});

18
test/fixtures/models/city.js vendored Normal file
View File

@ -0,0 +1,18 @@
export default (sequelize, DataTypes) => {
return sequelize.define('City', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: DataTypes.STRING,
}, {
classMethods: {
associate: (models) => {
models.City.hasMany(models.Team, {
foreignKey: { name: 'cityId' },
});
},
},
});
};

32
test/fixtures/models/player.js vendored Normal file
View File

@ -0,0 +1,32 @@
export default (sequelize, DataTypes) => {
return sequelize.define('Player', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: DataTypes.STRING,
teamId: DataTypes.INTEGER,
active: DataTypes.BOOLEAN,
}, {
classMethods: {
associate: (models) => {
models.Player.belongsTo(models.Team, {
foreignKey: { name: 'teamId' },
});
},
},
scopes: {
returnsOne: {
where: {
active: true,
},
},
returnsNone: {
where: {
name: 'notaname',
},
},
},
});
};

22
test/fixtures/models/team.js vendored Normal file
View File

@ -0,0 +1,22 @@
export default (sequelize, DataTypes) => {
return sequelize.define('Team', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: DataTypes.STRING,
cityId: DataTypes.INTEGER,
}, {
classMethods: {
associate: (models) => {
models.Team.belongsTo(models.City, {
foreignKey: { name: 'cityId' },
});
models.Team.hasMany(models.Player, {
foreignKey: { name: 'teamId' },
});
},
},
});
};

72
test/integration-setup.js Normal file
View File

@ -0,0 +1,72 @@
import hapi from 'hapi';
import Sequelize from 'sequelize';
import portfinder from 'portfinder';
import path from 'path';
import Promise from 'bluebird';
const getPort = Promise.promisify(portfinder.getPort);
const modelsPath = path.join(__dirname, 'fixtures', 'models');
const modelsGlob = path.join(modelsPath, '**', '*.js');
const dbName = 'db';
// these are what's in the fixtures dir
const modelNames = [
{ Singluar: 'City', singular: 'city', Plural: 'Cities', plural: 'cities' },
{ Singluar: 'Team', singular: 'team', Plural: 'Teams', plural: 'teams' },
{ Singluar: 'Player', singular: 'player', Plural: 'Players', plural: 'players' },
];
export default (test) => {
test.beforeEach('get an open port', async (t) => {
t.context.port = await getPort();
});
test.beforeEach('setup server', async (t) => {
const sequelize = t.context.sequelize = new Sequelize({
dialect: 'sqlite',
logging: false,
});
const server = t.context.server = new hapi.Server();
server.connection({
host: '0.0.0.0',
port: t.context.port,
});
await server.register({
register: require('hapi-sequelize'),
options: {
name: dbName,
models: [modelsGlob],
sequelize,
sync: true,
forceSync: true,
},
});
await server.register({
register: require('../src/index.js'),
options: {
name: dbName,
},
},
);
});
test.beforeEach('create data', async (t) => {
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 player1 = await Player.create({
name: 'Pinot', teamId: team1.id, active: true,
});
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
test.afterEach('stop the server', (t) => t.context.server.stop());
return { modelNames };
};