Compare commits
47 Commits
v2.7.0
...
labibramad
Author | SHA1 | Date | |
---|---|---|---|
05793eb749 | |||
e632f79e2b | |||
6fa9e90ec5 | |||
49d24ea265 | |||
11306667d6 | |||
1977304287 | |||
a141a38fe5 | |||
72452a0088 | |||
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 | |||
eebf7b91f0 | |||
a45a3ab317 | |||
7a8cd26dc8 | |||
80d0a74c82 | |||
863aa1d98b | |||
90f72cb07a | |||
d3976fa44b | |||
966b35164f | |||
548a6ecd98 | |||
be993eda40 | |||
bcb7861061 | |||
07176018b7 | |||
83eadf0929 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -36,3 +36,6 @@ npm-debug.log
|
||||
|
||||
# System
|
||||
.DS_Store
|
||||
|
||||
coverage.lcov
|
||||
.nyc_output
|
||||
|
87
README.md
87
README.md
@ -109,21 +109,87 @@ Getting related models is easy, just use a query parameter `include`.
|
||||
|
||||
```js
|
||||
// returns all teams with their related City model
|
||||
// GET /teams?include=City
|
||||
// GET /teams?include=city or
|
||||
// GET /teams?include={"model": "City"}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: City})
|
||||
|
||||
or if association defined with an alias
|
||||
// GET /players?include={"model": "Master", "as": "Couch"}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Players.findAll({include: Master, as: 'Couch'})
|
||||
```
|
||||
|
||||
If you want to get multiple related models, just pass multiple `include` parameters.
|
||||
```js
|
||||
// returns all teams with their related City and Uniform models
|
||||
// GET /teams?include=City&include=Uniform
|
||||
// GET /teams?include[]=city&include[]=uniform
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: [City, Uniform]})
|
||||
```
|
||||
|
||||
For models that have a many-to-many relationship, you can also pass the plural version of the association.
|
||||
```js
|
||||
// returns all teams with their related City and Uniform models
|
||||
// GET /teams?include=players
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: [Player]})
|
||||
```
|
||||
|
||||
Filtering by related models property, you can pass **where** paremeter inside each **include** items object.
|
||||
```js
|
||||
// returns all team with their related City where City property name equals Healdsburg
|
||||
// GET /teams?include={"model": "City", "where": {"name": "Healdsburg"}}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}})
|
||||
```
|
||||
|
||||
More complex example with nested include, association alias and association filtering.
|
||||
```js
|
||||
// returns all team with its players along with its couch of each player
|
||||
// GET /cities?include[]={
|
||||
// "model": "Team",
|
||||
// "include": {
|
||||
// "model": "Player",
|
||||
// "where": {
|
||||
// "name": "Pinot"
|
||||
// },
|
||||
// "include": {
|
||||
// "model": "Master",
|
||||
// "as": "Coach",
|
||||
// "where": {
|
||||
// "name": "Shifu"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// results in a Sequelize query:
|
||||
City.findAll({
|
||||
include: {
|
||||
model: Team,
|
||||
include: {
|
||||
model: Player,
|
||||
where: {
|
||||
name: Pinot
|
||||
},
|
||||
include: {
|
||||
model: Master,
|
||||
as: 'Coach',
|
||||
where: {
|
||||
name: 'Shifu'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## `limit` and `offset` queries
|
||||
Restricting list (`GET`) and scope queries to a restricted count can be done by passing `limit=<number>` and/or `offset=<number>`.
|
||||
|
||||
@ -149,6 +215,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']]})
|
||||
@ -162,6 +229,22 @@ Team.findAll({order: [['name', 'DESC']]})
|
||||
Team.findAll({order: [['name'], ['city']]})
|
||||
```
|
||||
|
||||
You can even order by associated models. Though there is a [sequelize bug](https://github.com/sequelize/sequelize/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20order%20join%20) that might prevent this from working properly. A workaround is to `&include` the model you're ordering by.
|
||||
```js
|
||||
// returns the players ordered by the team name
|
||||
// GET /players?order[0]={"model": "Team"}&order[0]=name
|
||||
|
||||
// results in a Sequelize query:
|
||||
Player.findAll({order: [[{model: Team}, 'name']]})
|
||||
|
||||
// if the above returns a Sequelize error: `No such column Team.name`,
|
||||
// you can work around this by forcing the join into the query:
|
||||
// GET /players?order[0]={"model": "Team"}&order[0]=name&include=team
|
||||
|
||||
// results in a Sequelize query:
|
||||
Player.findAll({order: [[{model: Team}, 'name']], include: [Team]})
|
||||
```
|
||||
|
||||
|
||||
## Authorization and other hooks
|
||||
You can use Hapi's [`ext` option](http://hapijs.com/api#route-options) to interact with the request both before and after this module does. This is useful if you want to enforce authorization, or modify the request before or after this module does. Hapi [has a full list of hooks](http://hapijs.com/api#request-lifecycle) you can use.
|
||||
|
@ -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
|
||||
|
35
package.json
35
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hapi-sequelize-crud",
|
||||
"version": "2.7.0",
|
||||
"version": "2.9.1",
|
||||
"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
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
|
||||
|
@ -19,14 +19,14 @@ export default (server, a, b, names, options) => {
|
||||
update(server, a, b, names);
|
||||
};
|
||||
|
||||
export const get = (server, a, b, names) => {
|
||||
export const get = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
@ -51,14 +51,14 @@ export const get = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const list = (server, a, b, names) => {
|
||||
export const list = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -77,16 +77,16 @@ export const list = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scope = (server, a, b, names) => {
|
||||
export const scope = async (server, a, b, names) => {
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -116,7 +116,7 @@ export const scope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scopeScope = (server, a, b, names) => {
|
||||
export const scopeScope = async (server, a, b, names) => {
|
||||
const scopes = {
|
||||
a: Object.keys(a.options.scopes),
|
||||
b: Object.keys(b.options.scopes),
|
||||
@ -124,11 +124,11 @@ export const scopeScope = (server, a, b, names) => {
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
||||
path: `${prefix}${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const list = await b.scope(request.params.scopeb).findAll({
|
||||
@ -152,14 +152,14 @@ export const scopeScope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (server, a, b, names) => {
|
||||
export const destroy = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -179,16 +179,16 @@ export const destroy = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyScope = (server, a, b, names) => {
|
||||
export const destroyScope = async (server, a, b, names) => {
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -221,14 +221,14 @@ export const destroyScope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const update = (server, a, b, names) => {
|
||||
export const update = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
|
@ -14,14 +14,14 @@ export default (server, a, b, names, options) => {
|
||||
update(server, a, b, names);
|
||||
};
|
||||
|
||||
export const get = (server, a, b, names) => {
|
||||
export const get = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -47,7 +47,7 @@ export const get = (server, a, b, names) => {
|
||||
export const create = (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`,
|
||||
path: `${prefix}${names.a.singular}/{id}/${names.b.singular}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
@ -67,14 +67,14 @@ export const create = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (server, a, b, names) => {
|
||||
export const destroy = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -99,11 +99,11 @@ export const destroy = (server, a, b, names) => {
|
||||
export const update = (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
|
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));
|
||||
});
|
162
src/crud-include.integration.test.js
Normal file
162
src/crud-include.integration.test.js
Normal file
@ -0,0 +1,162 @@
|
||||
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('belongsTo with alias /player?include={"model": "Master", "as": "Coach"}', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, master1 } = instances;
|
||||
const path = `/player/${team1.id}?include={"model": "Master", "as": "Coach"}`;
|
||||
|
||||
const { result, statusCode } = await server.inject(path);
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, team1.id);
|
||||
t.is(result.Coach.id, master1.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);
|
||||
});
|
||||
|
||||
test('multiple includes /team?include[]=players&include[]={"model": "City"}', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, player1, player2, city1 } = instances;
|
||||
const path = `/team/${team1.id}?include[]=players&include[]={"model": "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('include filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||
, async(t) => {
|
||||
const { server } = t.context;
|
||||
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
});
|
||||
|
||||
test('nested include filter ' +
|
||||
'/citiy?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, team1, team2 } = instances;
|
||||
const url = '/city?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, city1.id);
|
||||
|
||||
const teamIds = result.Teams.map(({ id }) => id);
|
||||
t.truthy(teamIds.includes(team1.id));
|
||||
t.truthy(teamIds.includes(team2.id));
|
||||
});
|
||||
|
||||
test('complex include ' +
|
||||
'/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, master1, player2, team1 } = instances;
|
||||
const method = 'GET';
|
||||
const url = '/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result[0].id, city1.id);
|
||||
t.is(result[0].Teams[0].id, team1.id);
|
||||
t.is(result[0].Teams[0].Players[0].id, player2.id);
|
||||
t.is(result[0].Teams[0].Players[0].Coach.id, master1.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'));
|
||||
});
|
26
src/crud-route-creation.integration.test.js
Normal file
26
src/crud-route-creation.integration.test.js
Normal 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' });
|
||||
});
|
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));
|
||||
});
|
||||
|
122
src/crud.js
122
src/crud.js
@ -5,16 +5,16 @@ import _ from 'lodash';
|
||||
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
|
||||
import { notFound } from 'boom';
|
||||
import * as associations from './associations/index';
|
||||
import getConfigForMethod from './get-config-for-method.js';
|
||||
import getConfigForMethod, { sequelizeOperators } from './get-config-for-method.js';
|
||||
|
||||
const createAll = ({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
scopes,
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
scopes,
|
||||
}) => {
|
||||
Object.keys(methods).forEach((method) => {
|
||||
methods[method]({
|
||||
@ -35,37 +35,65 @@ const createAll = ({
|
||||
export { associations };
|
||||
|
||||
/*
|
||||
The `models` option, becomes `permissions`, and can look like:
|
||||
The `models` option, becomes `permissions`, and can look like:
|
||||
|
||||
```
|
||||
models: ['cat', 'dog']
|
||||
```
|
||||
```
|
||||
models: ['cat', 'dog']
|
||||
```
|
||||
|
||||
or
|
||||
or
|
||||
|
||||
```
|
||||
models: {
|
||||
cat: ['list', 'get']
|
||||
, dog: true // all
|
||||
}
|
||||
```
|
||||
```
|
||||
models: {
|
||||
cat: ['list', 'get']
|
||||
, dog: true // all
|
||||
}
|
||||
```
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
|
||||
const modelName = model._singular;
|
||||
const modelAttributes = Object.keys(model.attributes);
|
||||
const modelAssociations = Object.keys(model.associations);
|
||||
const associatedModelNames = Object.keys(model.associations);
|
||||
const associatedModelAliases = _.map(model.associations, (assoc => assoc.as));
|
||||
const modelAssociations = [
|
||||
...associatedModelNames,
|
||||
..._.flatMap(associatedModelNames, (associationName) => {
|
||||
const { target } = model.associations[associationName];
|
||||
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 modelsHasAssociations = modelAssociations && modelAssociations.length;
|
||||
const validAssociationsString = modelsHasAssociations
|
||||
? joi.string().valid(...modelAssociations)
|
||||
: joi.valid(null);
|
||||
const validAssociationsObject = modelsHasAssociations
|
||||
? joi.object().keys({
|
||||
model: joi.string().valid(...modelAssociations),
|
||||
where: joi.object().keys({
|
||||
...attributeValidation,
|
||||
...sequelizeOperators,
|
||||
}),
|
||||
as: joi.string().valid(...associatedModelAliases),
|
||||
include: joi.any(), // @Todo: should validate the same as associationValidation var below
|
||||
})
|
||||
: joi.valid(null);
|
||||
const associationValidation = {
|
||||
include: joi.array().items(joi.string().valid(...modelAssociations)),
|
||||
include: [
|
||||
joi.array().items(validAssociationsString, validAssociationsObject),
|
||||
validAssociationsString,
|
||||
validAssociationsObject,
|
||||
],
|
||||
};
|
||||
|
||||
const scopes = Object.keys(model.options.scopes);
|
||||
|
||||
// if we don't have any permissions set, just create all the methods
|
||||
@ -79,11 +107,11 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
associationValidation,
|
||||
scopes,
|
||||
});
|
||||
// if permissions are set, but we can't parse them, throw an error
|
||||
// if permissions are set, but we can't parse them, throw an error
|
||||
} else if (!Array.isArray(permissions)) {
|
||||
throw new Error('hapi-sequelize-crud: `models` property must be an array');
|
||||
// if permissions are set, but the only thing we've got is a model name, there
|
||||
// are no permissions to be set, so just create all methods and move on
|
||||
// if permissions are set, but the only thing we've got is a model name, there
|
||||
// are no permissions to be set, so just create all methods and move on
|
||||
} else if (permissions.includes(modelName)) {
|
||||
createAll({
|
||||
server,
|
||||
@ -94,7 +122,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
associationValidation,
|
||||
scopes,
|
||||
});
|
||||
// if we've gotten here, we have complex permissions and need to set them
|
||||
// if we've gotten here, we have complex permissions and need to set them
|
||||
} else {
|
||||
const permissionOptions = permissions.filter((permission) => {
|
||||
return permission.model === modelName;
|
||||
@ -135,14 +163,14 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
}
|
||||
};
|
||||
|
||||
export const list = ({ server, model, prefix = '/', config }) => {
|
||||
export const list = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._plural),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { limit, offset } = parseLimitAndOffset(request);
|
||||
const order = parseOrder(request);
|
||||
@ -153,6 +181,8 @@ export const list = ({ server, model, prefix = '/', config }) => {
|
||||
where, include, limit, offset, order,
|
||||
});
|
||||
|
||||
if (!list.length) return void reply(notFound('Nothing found.'));
|
||||
|
||||
reply(list.map((item) => item.toJSON()));
|
||||
},
|
||||
|
||||
@ -160,14 +190,14 @@ export const list = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const get = ({ server, model, prefix = '/', config }) => {
|
||||
export const get = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._singular, '{id?}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { id } = request.params;
|
||||
if (id) where[model.primaryKeyField] = id;
|
||||
@ -184,14 +214,14 @@ export const get = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scope = ({ server, model, prefix = '/', config }) => {
|
||||
export const scope = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._plural, '{scope}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { limit, offset } = parseLimitAndOffset(request);
|
||||
const order = parseOrder(request);
|
||||
@ -202,6 +232,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,
|
||||
@ -232,10 +264,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());
|
||||
@ -254,9 +294,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());
|
||||
@ -267,20 +315,22 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyScope = ({ server, model, prefix = '/', config }) => {
|
||||
export const destroyScope = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: path.join(prefix, model._plural, '{scope}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
if (include instanceof Error) return void reply(include);
|
||||
|
||||
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());
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
@ -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')
|
||||
);
|
||||
|
12
src/index.js
12
src/index.js
@ -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];
|
||||
@ -32,11 +32,12 @@ const register = (server, options = {}, next) => {
|
||||
const { plural, singular } = model.options.name;
|
||||
model._plural = plural.toLowerCase();
|
||||
model._singular = singular.toLowerCase();
|
||||
model._Plural = plural;
|
||||
model._Singular = singular;
|
||||
|
||||
// Join tables
|
||||
if (model.options.name.singular !== model.name) continue;
|
||||
|
||||
crud(server, model, options);
|
||||
|
||||
for (const key of Object.keys(model.associations)) {
|
||||
const association = model.associations[key];
|
||||
@ -92,6 +93,13 @@ const register = (server, options = {}, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// build the methods for each model now that we've defined all the
|
||||
// associations
|
||||
Object.keys(models).forEach((modelName) => {
|
||||
const model = models[modelName];
|
||||
crud(server, model, options);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
112
src/utils.js
112
src/utils.js
@ -1,33 +1,85 @@
|
||||
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
|
||||
import { notImplemented } from 'boom';
|
||||
import joi from 'joi';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
|
||||
|
||||
export const parseInclude = request => {
|
||||
const include = Array.isArray(request.query.include)
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
;
|
||||
|
||||
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.');
|
||||
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
|
||||
}
|
||||
|
||||
const { models } = noGetDb ? request : request.getDb();
|
||||
|
||||
return include.map(a => {
|
||||
if (typeof a === 'string') return models[a];
|
||||
return models;
|
||||
};
|
||||
|
||||
if (a && typeof a.model === 'string' && a.model.length) {
|
||||
a.model = models[a.model];
|
||||
const getModelInstance = (models, includeItem) => {
|
||||
return new Promise(async(resolve) => {
|
||||
if (includeItem) {
|
||||
if (typeof includeItem !== 'object') {
|
||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
||||
const { _singular, _plural } = models[modelName];
|
||||
return _singular === includeItem || _plural === includeItem;
|
||||
});
|
||||
|
||||
if (singluarOrPluralMatch) {
|
||||
return resolve(models[singluarOrPluralMatch]);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof includeItem === 'string' && models.hasOwnProperty(includeItem)) {
|
||||
return resolve(models[includeItem]);
|
||||
} else if (typeof includeItem === 'object') {
|
||||
if (
|
||||
typeof includeItem.model === 'string' &&
|
||||
includeItem.model.length &&
|
||||
models.hasOwnProperty(includeItem.model)
|
||||
) {
|
||||
includeItem.model = models[includeItem.model];
|
||||
}
|
||||
if (includeItem.hasOwnProperty('include')) {
|
||||
includeItem.include = await getModelInstance(models, includeItem.include);
|
||||
return resolve(includeItem);
|
||||
} else {
|
||||
return resolve(includeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolve(includeItem);
|
||||
});
|
||||
};
|
||||
|
||||
export const parseInclude = async(request) => {
|
||||
if (typeof request.query.include === 'undefined') return [];
|
||||
|
||||
const include = Array.isArray(request.query.include)
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
;
|
||||
|
||||
const models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
|
||||
const jsonValidation = joi.string().regex(/^\{.*?"model":.*?\}$/);
|
||||
const includes = include.map(async(b) => {
|
||||
let a = b;
|
||||
try {
|
||||
if (!jsonValidation.validate(a).error) {
|
||||
a = JSON.parse(b);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
return a;
|
||||
return getModelInstance(models, a);
|
||||
}).filter(identity);
|
||||
|
||||
return await Promise.all(includes);
|
||||
};
|
||||
|
||||
export const parseWhere = request => {
|
||||
@ -56,24 +108,40 @@ export const parseLimitAndOffset = (request) => {
|
||||
return out;
|
||||
};
|
||||
|
||||
const parseOrderArray = (order, models) => {
|
||||
return order.map((requestColumn) => {
|
||||
if (Array.isArray(requestColumn)) {
|
||||
return parseOrderArray(requestColumn, models);
|
||||
}
|
||||
|
||||
let column;
|
||||
try {
|
||||
column = JSON.parse(requestColumn);
|
||||
} catch (e) {
|
||||
column = requestColumn;
|
||||
}
|
||||
|
||||
if (column.model) column.model = models[column.model];
|
||||
|
||||
return column;
|
||||
});
|
||||
};
|
||||
|
||||
export const parseOrder = (request) => {
|
||||
const { order } = request.query;
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
const models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
|
||||
// transform to an array so sequelize will escape the input for us and
|
||||
// maintain security. See http://docs.sequelizejs.com/en/latest/docs/querying/#ordering
|
||||
if (isString(order)) return order.split(' ');
|
||||
const requestOrderColumns = isString(order) ? [order.split(' ')] : order;
|
||||
|
||||
for (const key of Object.keys(order)) {
|
||||
try {
|
||||
order[key] = JSON.parse(order[key]);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
const parsedOrder = parseOrderArray(requestOrderColumns, models);
|
||||
|
||||
return order;
|
||||
return parsedOrder;
|
||||
};
|
||||
|
||||
export const getMethod = (model, association, plural = true, method = 'get') => {
|
||||
|
@ -2,7 +2,8 @@ import test from 'ava';
|
||||
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
|
||||
|
||||
test.beforeEach((t) => {
|
||||
t.context.request = { query: {} };
|
||||
const models = t.context.models = { User: {} };
|
||||
t.context.request = { query: {}, models };
|
||||
});
|
||||
|
||||
test('parseLimitAndOffset is a function', (t) => {
|
||||
@ -57,19 +58,18 @@ test('parseOrder returns order when a string', (t) => {
|
||||
|
||||
t.deepEqual(
|
||||
parseOrder(request)
|
||||
, [order]
|
||||
, [[order]]
|
||||
);
|
||||
});
|
||||
|
||||
test('parseOrder returns order when json', (t) => {
|
||||
const { request } = t.context;
|
||||
const order = [{ model: 'User' }, 'DESC'];
|
||||
const { request,models } = t.context;
|
||||
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
|
||||
request.query.thing = 'hi';
|
||||
|
||||
t.deepEqual(
|
||||
parseOrder(request)
|
||||
, order
|
||||
, [{ model: models.User }, 'DESC']
|
||||
);
|
||||
});
|
||||
|
||||
|
18
test/fixtures/models/city.js
vendored
Normal file
18
test/fixtures/models/city.js
vendored
Normal 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' },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
18
test/fixtures/models/master.js
vendored
Normal file
18
test/fixtures/models/master.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
export default (sequelize, DataTypes) => {
|
||||
return sequelize.define('Master', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
models.Master.hasMany(models.Player, {
|
||||
foreignKey: 'coachId'
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
43
test/fixtures/models/player.js
vendored
Normal file
43
test/fixtures/models/player.js
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
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' },
|
||||
});
|
||||
models.Player.belongsTo(models.Master, {
|
||||
foreignKey: 'coachId',
|
||||
as: 'Coach',
|
||||
});
|
||||
},
|
||||
},
|
||||
scopes: {
|
||||
returnsOne: {
|
||||
where: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
returnsNone: {
|
||||
where: {
|
||||
name: 'notaname',
|
||||
},
|
||||
},
|
||||
returnsAll: {
|
||||
where: {
|
||||
name: {
|
||||
$ne: 'notaname',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
22
test/fixtures/models/team.js
vendored
Normal file
22
test/fixtures/models/team.js
vendored
Normal 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' },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
83
test/integration-setup.js
Normal file
83
test/integration-setup.js
Normal file
@ -0,0 +1,83 @@
|
||||
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' },
|
||||
{ Singluar: 'Master', singular: 'master', Plural: 'Masters', plural: 'masters' },
|
||||
];
|
||||
|
||||
|
||||
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, Master, 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 master1 = await Master.create({ name: 'Shifu' });
|
||||
const master2 = await Master.create({ name: 'Oogway' });
|
||||
const player1 = await Player.create({
|
||||
name: 'Cat', teamId: team1.id, active: true, coachId: master1.id
|
||||
});
|
||||
const player2 = await Player.create({
|
||||
name: 'Pinot', teamId: team1.id, coachId: master1.id
|
||||
});
|
||||
const player3 = await Player.create({
|
||||
name: 'Syrah', teamId: team2.id, coachId: master2.id
|
||||
});
|
||||
t.context.instances = {
|
||||
city1, team1, team2, player1, player2, player3, master1, master2
|
||||
};
|
||||
});
|
||||
|
||||
// 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 };
|
||||
};
|
Reference in New Issue
Block a user