4 Commits

Author SHA1 Message Date
a855665777 2.3.0 2016-07-21 18:01:54 -07:00
034287672c 2.2.0 2016-07-21 18:00:38 -07:00
e8c0e61c6b Fork to @getable 2016-07-21 18:00:38 -07:00
a64a55af0d Add: permissions
It's now possible to limit the models rest routes are created for. This
is done via a `models` option that can be simple to complex. The readme
has been updated to reflect this.
2016-07-21 18:00:38 -07:00
21 changed files with 140 additions and 1809 deletions

View File

@ -10,5 +10,5 @@
"transform-decorators-legacy",
"transform-es2015-modules-commonjs"
],
"sourceMaps": "inline"
"sourceMaps": true
}

View File

@ -1,9 +1,3 @@
{
"plugins": [
"ava"
],
"extends": [
"pichak",
"plugin:ava/recommended"
]
"extends": "pichak"
}

3
.gitignore vendored
View File

@ -33,6 +33,3 @@ node_modules
# Debug log from npm
npm-debug.log
# System
.DS_Store

View File

@ -1,7 +0,0 @@
Commit Message
===============
Please follow [this convention](http://karma-runner.github.io/1.0/dev/git-commit-msg.html) for git commit message.
Lint
====
Please lint your code using `npm run lint` (also `npm run lint -- --fix` to auto-fix).

176
README.md
View File

@ -1,4 +1,4 @@
hapi-sequelize-crud [![CircleCI](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud.svg?style=svg)](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud)
hapi-sequelize-crud
===================
Automatically generate a RESTful API for your models and associations
@ -11,9 +11,6 @@ npm install -S hapi-sequelize-crud
##Configure
Please note that you should register `hapi-sequelize-crud` after defining your
associations.
```javascript
// First, register hapi-sequelize
await register({
@ -33,170 +30,36 @@ await register({
prefix: '/v1',
name: 'db', // the same name you used for configuring `hapi-sequelize` (options.name)
defaultConfig: { ... }, // passed as `config` to all routes created
// You can specify which models must have routes defined for using the
// `models` property. If you omit this property, all models will have
// models defined for them. e.g.
models: ['cat', 'dog'] // only the cat and dog models will have routes created
// or
models: [
models: {
// possible methods: list, get, scope, create, destroy, destroyAll, destroyScope, update
// the cat model only has get and list methods enabled
{model: 'cat', methods: ['get', 'list']},
// the dog model has all methods enabled
{model: 'dog'},
// the cow model also has all methods enabled
'cow',
// the bat model as a custom config for the list method, but uses the default config for create.
// `config` if provided, overrides the default config
{model: 'bat', methods: ['list'], config: { ... }},
{model: 'bat', methods: ['create']}
{model: 'fly', config: {
// interact with the request before hapi-sequelize-crud does
, ext: {
onPreHandler: (request, reply) => {
if (request.auth.hasAccessToFly) reply.continue()
else reply(Boom.unauthorized())
cat: ['get', 'list'], // the cat model only has get and list methods enabled
dog: true, // the dog model has all methods enabled
bat: {
methods: ['list'],
config: { ... } // if provided, overrides the default config
}
}
// change the response data
response: {
schema: {id: joi.string()},
modify: true
}
}}
]
}
});
```
### Methods
* **list**: get all rows in a table
* **get**: get a single row
* **scope**: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model)
* **create**: create a new row
* **destroy**: delete a row
* **destroyAll**: delete all models in the table
* **destroyScope**: use a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model) to find rows, then delete them
* **update**: update a row
## `where` queries
It's easy to restrict your requests using Sequelize's `where` query option. Just pass a query parameter.
```js
// returns only teams that have a `city` property of "windsor"
// GET /team?city=windsor
// results in the Sequelize query:
Team.findOne({ where: { city: 'windsor' }})
```
You can also do more complex queries by setting the value of a key to JSON.
```js
// returns only teams that have a `address.city` property of "windsor"
// GET /team?city={"address": "windsor"}
// or
// GET /team?city[address]=windsor
// results in the Sequelize query:
Team.findOne({ where: { address: { city: 'windsor' }}})
```
## `include` queries
Getting related models is easy, just use a query parameter `include`.
```js
// returns all teams with their related City model
// GET /teams?include=city
// results in a Sequelize query:
Team.findAll({include: City})
```
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
// 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]})
```
## `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>`.
```js
// returns 10 teams starting from the 10th
// GET /teams?limit=10&offset=10
// results in a Sequelize query:
Team.findAll({limit: 10, offset: 10})
```
## `order` queries
You can change the order of the resulting query by passing `order` to the query.
```js
// returns the teams ordered by the name column
// GET /teams?order[]=name
// results in a Sequelize query:
Team.findAll({order: ['name']})
```
```js
// returns the teams ordered by the name column, descending
// GET /teams?order[0]=name&order[0]=DESC
// results in a Sequelize query:
Team.findAll({order: [['name', 'DESC']]})
```
```js
// returns the teams ordered by the name, then the city columns, descending
// GET /teams?order[0]=name&order[1]=city
// results in a Sequelize query:
Team.findAll({order: [['name'], ['city']]})
```
* list: get all rows in a table
* get: get a single row
* scope: reference a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model)
* create: create a new row
* destroy: delete a row
* destroyAll: delete all models in the table
* destroyScope: use a [sequelize scope](http://docs.sequelizejs.com/en/latest/api/model/#scopeoptions-model) to find rows, then delete them
* update: update a row
## 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.
Please note that you should register `hapi-sequelize-crud` after defining your
associations.
## Modify the response format
By default, `hapi-sequelize-crud` routes will respond with the full model. You can modify this using the built-in [hapi settings](http://hapijs.com/tutorials/validation#output).
```js
await register({
register: require('hapi-sequelize-crud'),
options: {
{model: 'fly', config: {
response: {
// setting this schema will restrict the response to only the id
schema: { id: joi.string() },
// This tells Hapi to restrict the response to the keys specified in `schema`
modify: true
}
}}
}
})
```
## Full list of methods
##What do I get
Let's say you have a `many-to-many` association like this:
@ -211,9 +74,8 @@ You get these:
# get an array of records
GET /team/{id}/roles
GET /role/{id}/teams
# might also append `where` query parameters to search for
# might also append query parameters to search for
GET /role/{id}/teams?members=5
GET /role/{id}/teams?city=healdsburg
# you might also use scopes
GET /teams/{scope}/roles/{scope}

View File

@ -1,9 +0,0 @@
machine:
node:
version: 6.5.0
dependencies:
pre:
- npm prune
post:
- mkdir -p $CIRCLE_TEST_REPORTS/ava

View File

@ -1,6 +1,6 @@
{
"name": "hapi-sequelize-crud",
"version": "2.7.2",
"name": "@getable/hapi-sequelize-crud",
"version": "2.3.0",
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
"main": "build/index.js",
"config": {
@ -9,14 +9,13 @@
}
},
"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",
"lint": "eslint src test",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "scripty",
"watch": "scripty"
},
"repository": {
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
"git": "https://github.com/Getable/hapi-sequelize-crud"
},
"files": [
"build"
@ -24,35 +23,21 @@
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
"license": "MIT",
"devDependencies": {
"ava": "^0.16.0",
"babel-cli": "^6.16.0",
"babel-cli": "^6.10.1",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-closure-elimination": "^1.0.6",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"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",
"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",
"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"
"babel-plugin-transform-es2015-modules-commonjs": "^6.10.3",
"babel-preset-stage-1": "^6.5.0",
"eslint": "2.10.2",
"eslint-config-pichak": "1.1.0",
"ghooks": "1.0.3",
"scripty": "^1.6.0"
},
"dependencies": {
"boom": "^4.2.0",
"joi": "^9.2.0",
"lodash": "^4.16.4"
},
"optionalDependencies": {
"babel-polyfill": "^6.13.0"
"babel": "5.8.3",
"boom": "^3.2.2",
"joi": "7.2.1",
"lodash": "4.0.0"
}
}

View File

@ -1,85 +0,0 @@
import test from 'ava';
import 'sinon-bluebird';
import setup from '../test/integration-setup.js';
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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, response } = await server.inject(path);
t.falsy(response instanceof Error);
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

@ -1,26 +0,0 @@
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

@ -1,39 +1,15 @@
import joi from 'joi';
import path from 'path';
import error from './error';
import _ from 'lodash';
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
import { parseInclude, parseWhere } from './utils';
import { notFound } from 'boom';
import * as associations from './associations/index';
import getConfigForMethod from './get-config-for-method.js';
const createAll = ({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
}) => {
const createAll = ({server, model, prefix, config}) => {
Object.keys(methods).forEach((method) => {
methods[method]({
server,
model,
prefix,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
config,
scopes,
}),
});
methods[method]({server, model, prefix, config});
});
};
export { associations };
/*
The `models` option, becomes `permissions`, and can look like:
@ -54,128 +30,66 @@ models: {
export default (server, model, {prefix, defaultConfig: config, models: permissions}) => {
const modelName = model._singular;
const modelAttributes = Object.keys(model.attributes);
const associatedModelNames = Object.keys(model.associations);
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 validAssociations = modelAssociations.length
? joi.string().valid(...modelAssociations)
: joi.valid(null);
const associationValidation = {
include: [joi.array().items(validAssociations), validAssociations],
};
const scopes = Object.keys(model.options.scopes);
// if we don't have any permissions set, just create all the methods
if (!permissions) {
createAll({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
});
// 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
} else if (permissions.includes(modelName)) {
createAll({
server,
model,
prefix,
config,
attributeValidation,
associationValidation,
scopes,
});
// if we've gotten here, we have complex permissions and need to set them
} else {
const permissionOptions = permissions.filter((permission) => {
return permission.model === modelName;
});
createAll({server, model, prefix, config});
}
else if (Array.isArray(permissions) && permissions.includes(modelName)) {
createAll({server, model, prefix, config});
}
else if (_.isPlainObject(permissions)) {
const permittedModels = Object.keys(permissions);
permissionOptions.forEach((permissionOption) => {
if (_.isPlainObject(permissionOption)) {
const permissionConfig = permissionOption.config || config;
if (permissionOption.methods) {
permissionOption.methods.forEach((method) => {
if (permissions[modelName] === true) {
createAll({server, model, prefix, config});
}
else if (permittedModels.includes(modelName)) {
if (Array.isArray(permissions[modelName])) {
permissions[modelName].forEach((method) => {
methods[method]({server, model, prefix, config});
});
}
else if (_.isPlainObject(permissions[modelName])) {
permissions[modelName].methods.forEach((method) => {
methods[method]({
server,
model,
prefix,
config: getConfigForMethod({
method,
attributeValidation,
associationValidation,
scopes,
config: permissionConfig,
}),
config: permissions[modelName].config || config,
});
});
} else {
createAll({
server,
model,
prefix,
attributeValidation,
associationValidation,
scopes,
config: permissionConfig,
});
}
}
});
}
};
export const list = ({ server, model, prefix = '/', config }) => {
export const list = ({server, model, prefix, config}) => {
server.route({
method: 'GET',
path: path.join(prefix, model._plural),
path: `${prefix}/${model._plural}`,
@error
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
const { limit, offset } = parseLimitAndOffset(request);
const order = parseOrder(request);
if (include instanceof Error) return void reply(include);
const list = await model.findAll({
where, include, limit, offset, order,
where, include,
});
reply(list.map((item) => item.toJSON()));
reply(list);
},
config,
});
};
export const get = ({ server, model, prefix = '/', config }) => {
export const get = ({server, model, prefix, config}) => {
server.route({
method: 'GET',
path: path.join(prefix, model._singular, '{id?}'),
path: `${prefix}/${model._singular}/{id?}`,
@error
async handler(request, reply) {
@ -184,62 +98,68 @@ export const get = ({ server, model, prefix = '/', config }) => {
const { id } = request.params;
if (id) where[model.primaryKeyField] = id;
if (include instanceof Error) return void reply(include);
const instance = await model.findOne({ where, include });
if (!instance) return void reply(notFound(`${id} not found.`));
reply(instance.toJSON());
reply(instance);
},
config,
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
id: joi.any(),
}),
},
}, config),
});
};
export const scope = ({ server, model, prefix = '/', config }) => {
export const scope = ({server, model, prefix, config}) => {
const scopes = Object.keys(model.options.scopes);
server.route({
method: 'GET',
path: path.join(prefix, model._plural, '{scope}'),
path: `${prefix}/${model._plural}/{scope}`,
@error
async handler(request, reply) {
const include = parseInclude(request);
const where = parseWhere(request);
const { limit, offset } = parseLimitAndOffset(request);
const order = parseOrder(request);
if (include instanceof Error) return void reply(include);
const list = await model.scope(request.params.scope).findAll({ include, where });
const list = await model.scope(request.params.scope).findAll({
include, where, limit, offset, order,
});
reply(list.map((item) => item.toJSON()));
reply(list);
},
config,
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
scope: joi.string().valid(...scopes),
}),
},
}, config),
});
};
export const create = ({ server, model, prefix = '/', config }) => {
export const create = ({server, model, prefix, config}) => {
server.route({
method: 'POST',
path: path.join(prefix, model._singular),
path: `${prefix}/${model._singular}`,
@error
async handler(request, reply) {
const instance = await model.create(request.payload);
reply(instance.toJSON());
reply(instance);
},
config,
});
};
export const destroy = ({ server, model, prefix = '/', config }) => {
export const destroy = ({server, model, prefix, config}) => {
server.route({
method: 'DELETE',
path: path.join(prefix, model._singular, '{id?}'),
path: `${prefix}/${model._singular}/{id?}`,
@error
async handler(request, reply) {
@ -250,18 +170,17 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
await Promise.all(list.map(instance => instance.destroy()));
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
reply(list.length === 1 ? list[0] : list);
},
config,
});
};
export const destroyAll = ({ server, model, prefix = '/', config }) => {
export const destroyAll = ({server, model, prefix, config}) => {
server.route({
method: 'DELETE',
path: path.join(prefix, model._plural),
path: `${prefix}/${model._plural}`,
@error
async handler(request, reply) {
@ -271,58 +190,73 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
await Promise.all(list.map(instance => instance.destroy()));
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
reply(list.length === 1 ? list[0] : list);
},
config,
});
};
export const destroyScope = ({ server, model, prefix = '/', config }) => {
export const destroyScope = ({server, model, prefix, config}) => {
const scopes = Object.keys(model.options.scopes);
server.route({
method: 'DELETE',
path: path.join(prefix, model._plural, '{scope}'),
path: `${prefix}/${model._plural}/{scope}`,
@error
async handler(request, reply) {
const include = 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 });
await Promise.all(list.map(instance => instance.destroy()));
const listAsJSON = list.map((item) => item.toJSON());
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
reply(list);
},
config,
config: _.defaultsDeep({
validate: {
params: joi.object().keys({
scope: joi.string().valid(...scopes),
}),
},
}, config),
});
};
export const update = ({ server, model, prefix = '/', config }) => {
export const update = ({server, model, prefix, config}) => {
server.route({
method: 'PUT',
path: path.join(prefix, model._singular, '{id}'),
path: `${prefix}/${model._singular}/{id}`,
@error
async handler(request, reply) {
const { id } = request.params;
const instance = await model.findById(id);
const instance = await model.findOne({
where: {
id,
},
});
if (!instance) return void reply(notFound(`${id} not found.`));
await instance.update(request.payload);
reply(instance.toJSON());
reply(instance);
},
config,
config: _.defaultsDeep({
validate: {
payload: joi.object().required(),
},
}, config),
});
};
import * as associations from './associations/index';
export { associations };
const methods = {
list, get, scope, create, destroy, destroyAll, destroyScope, update,
};

View File

@ -1,231 +0,0 @@
import test from 'ava';
import { list } from './crud.js';
import { stub } from 'sinon';
import uniqueId from 'lodash/uniqueId.js';
import 'sinon-bluebird';
const METHODS = {
GET: 'GET',
};
test.beforeEach('setup server', (t) => {
t.context.server = {
route: stub(),
};
});
const makeModel = () => {
const id = uniqueId();
return {
findAll: stub(),
_plural: 'models',
_singular: 'model',
toJSON: () => ({ id }),
id,
};
};
test.beforeEach('setup model', (t) => {
t.context.model = makeModel();
});
test.beforeEach('setup models', (t) => {
t.context.models = [t.context.model, makeModel()];
});
test.beforeEach('setup request stub', (t) => {
t.context.request = {
query: {},
payload: {},
models: t.context.models,
};
});
test.beforeEach('setup reply stub', (t) => {
t.context.reply = stub();
});
test('crud#list without prefix', (t) => {
const { server, model } = t.context;
list({ server, model });
const { path } = server.route.args[0][0];
t.falsy(
path.includes('undefined'),
'correctly sets the path without a prefix defined',
);
t.is(
path,
`/${model._plural}`,
'the path sets to the plural model'
);
});
test('crud#list with prefix', (t) => {
const { server, model } = t.context;
const prefix = '/v1';
list({ server, model, prefix });
const { path } = server.route.args[0][0];
t.is(
path,
`${prefix}/${model._plural}`,
'the path sets to the plural model with the prefix'
);
});
test('crud#list method', (t) => {
const { server, model } = t.context;
list({ server, model });
const { method } = server.route.args[0][0];
t.is(
method,
METHODS.GET,
`sets the method to ${METHODS.GET}`
);
});
test('crud#list config', (t) => {
const { server, model } = t.context;
const userConfig = {};
list({ server, model, config: userConfig });
const { config } = server.route.args[0][0];
t.is(
config,
userConfig,
'sets the user config'
);
});
test('crud#list handler', async (t) => {
const { server, model, request, reply, models } = t.context;
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
try {
await handler(request, reply);
} catch (e) {
t.ifError(e, 'does not error while handling');
} finally {
t.pass('does not error while handling');
}
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
t.falsy(response instanceof Error, response);
t.deepEqual(
response,
models.map(({ id }) => ({ id })),
'responds with the list of models'
);
});
test('crud#list handler if parseInclude errors', async (t) => {
const { server, model, request, reply } = t.context;
// we _want_ the error
delete request.models;
list({ server, model });
const { handler } = server.route.args[0][0];
await handler(request, reply);
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
t.truthy(
response.isBoom,
'responds with a Boom error'
);
});
test('crud#list handler with limit', async (t) => {
const { server, model, request, reply, models } = t.context;
const { findAll } = model;
// set the limit
request.query.limit = 1;
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
try {
await handler(request, reply);
} catch (e) {
t.ifError(e, 'does not error while handling');
} finally {
t.pass('does not error while handling');
}
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
const findAllArgs = findAll.args[0][0];
t.falsy(response instanceof Error, response);
t.is(
findAllArgs.limit,
request.query.limit,
'queries with the limit'
);
});
test('crud#list handler with order', async (t) => {
const { server, model, request, reply, models } = t.context;
const { findAll } = model;
// set the limit
request.query.order = 'key';
list({ server, model });
const { handler } = server.route.args[0][0];
model.findAll.resolves(models);
try {
await handler(request, reply);
} catch (e) {
t.ifError(e, 'does not error while handling');
} finally {
t.pass('does not error while handling');
}
t.truthy(
reply.calledOnce
, 'calls reply only once'
);
const response = reply.args[0][0];
const findAllArgs = findAll.args[0][0];
t.falsy(response instanceof Error, response);
t.deepEqual(
findAllArgs.order,
[request.query.order],
'queries with the order as an array b/c that\'s what sequelize wants'
);
});

View File

@ -1,5 +1,3 @@
import Boom from 'boom';
export default (target, key, descriptor) => {
const fn = descriptor.value;
@ -7,60 +5,9 @@ export default (target, key, descriptor) => {
try {
await fn(request, reply);
} catch (e) {
if (e.original) {
const { code, detail, hint } = e.original;
let error;
// pg error codes https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
if (code && (code.startsWith('22') || code.startsWith('23'))) {
error = Boom.wrap(e, 406);
} else if (code && (code.startsWith('42'))) {
error = Boom.wrap(e, 422);
// TODO: we could get better at parse postgres error codes
} else {
// use a 502 error code since the issue is upstream with postgres, not
// this server
error = Boom.wrap(e, 502);
}
// detail tends to be more specific information. So, if we have it, use.
if (detail) {
error.message += `: ${detail}`;
error.reformat();
}
// hint might provide useful information about how to fix the problem
if (hint) {
error.message += ` Hint: ${hint}`;
error.reformat();
}
reply(error);
} else if (!e.isBoom) {
const { message } = e;
let err;
if (e.name === 'SequelizeValidationError')
err = Boom.badData(message);
else if (e.name === 'SequelizeConnectionTimedOutError')
err = Boom.gatewayTimeout(message);
else if (e.name === 'SequelizeHostNotReachableError')
err = Boom.serverUnavailable(message);
else if (e.name === 'SequelizeUniqueConstraintError')
err = Boom.conflict(message);
else if (e.name === 'SequelizeForeignKeyConstraintError')
err = Boom.expectationFailed(message);
else if (e.name === 'SequelizeExclusionConstraintError')
err = Boom.expectationFailed(message);
else if (e.name === 'SequelizeConnectionError')
err = Boom.badGateway(message);
else err = Boom.badImplementation(message);
reply(err);
} else {
console.error(e);
reply(e);
}
}
};
return descriptor;

View File

@ -1,156 +0,0 @@
import { set, get } from 'lodash';
import joi from 'joi';
// if the custom validation is a joi object we need to concat
// else, assume it's an plain object and we can just add it in with .keys
const concatToJoiObject = (joi, candidate) => {
if (!candidate) return joi;
else if (candidate.isJoi) return joi.concat(candidate);
else return joi.keys(candidate);
};
export const sequelizeOperators = {
$and: joi.any(),
$or: joi.any(),
$gt: joi.any(),
$gte: joi.any(),
$lt: joi.any(),
$lte: joi.any(),
$ne: joi.any(),
$eq: joi.any(),
$not: joi.any(),
$between: joi.any(),
$notBetween: joi.any(),
$in: joi.any(),
$notIn: joi.any(),
$like: joi.any(),
$notLike: joi.any(),
$iLike: joi.any(),
$notILike: joi.any(),
$overlap: joi.any(),
$contains: joi.any(),
$contained: joi.any(),
$any: joi.any(),
$col: joi.any(),
};
export const whereMethods = [
'list',
'get',
'scope',
'destroy',
'destoryScope',
'destroyAll',
];
export const includeMethods = [
'list',
'get',
'scope',
'destoryScope',
];
export const payloadMethods = [
'create',
'update',
];
export const scopeParamsMethods = [
'destroyScope',
'scope',
];
export const idParamsMethods = [
'get',
'update',
];
export const restrictMethods = [
'list',
'scope',
];
export default ({
method, attributeValidation, associationValidation, scopes = [], config = {},
}) => {
const hasWhere = whereMethods.includes(method);
const hasInclude = includeMethods.includes(method);
const hasPayload = payloadMethods.includes(method);
const hasScopeParams = scopeParamsMethods.includes(method);
const hasIdParams = idParamsMethods.includes(method);
const hasRestrictMethods = restrictMethods.includes(method);
// clone the config so we don't modify it on multiple passes.
let methodConfig = { ...config, validate: { ...config.validate } };
if (hasWhere) {
const query = concatToJoiObject(joi.object()
.keys({
...attributeValidation,
...sequelizeOperators,
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
if (hasInclude) {
const query = concatToJoiObject(joi.object()
.keys({
...associationValidation,
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
if (hasPayload) {
const payload = concatToJoiObject(joi.object()
.keys({
...attributeValidation,
}),
get(methodConfig, 'validate.payload')
);
methodConfig = set(methodConfig, 'validate.payload', payload);
}
if (hasScopeParams) {
const params = concatToJoiObject(joi.object()
.keys({
scope: joi.string().valid(...scopes),
}),
get(methodConfig, 'validate.params')
);
methodConfig = set(methodConfig, 'validate.params', params);
}
if (hasIdParams) {
const params = concatToJoiObject(joi.object()
.keys({
id: joi.any(),
}),
get(methodConfig, 'validate.params')
);
methodConfig = set(methodConfig, 'validate.params', params);
}
if (hasRestrictMethods) {
const query = concatToJoiObject(joi.object()
.keys({
limit: joi.number().min(0).integer(),
offset: joi.number().min(0).integer(),
order: joi.array(),
}),
get(methodConfig, 'validate.query')
);
methodConfig = set(methodConfig, 'validate.query', query);
}
return methodConfig;
};

View File

@ -1,580 +0,0 @@
import test from 'ava';
import joi from 'joi';
import
getConfigForMethod, {
whereMethods,
includeMethods,
payloadMethods,
scopeParamsMethods,
idParamsMethods,
restrictMethods,
sequelizeOperators,
} from './get-config-for-method.js';
test.beforeEach((t) => {
t.context.models = ['MyModel'];
t.context.scopes = ['aScope'];
t.context.attributeValidation = {
myKey: joi.any(),
};
t.context.associationValidation = {
include: joi.array().items(joi.string().valid(t.context.models)),
};
t.context.config = {
cors: {},
};
});
test('validate.query seqeulizeOperators', (t) => {
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { query } = configForMethod.validate;
t.truthy(
query,
`applies query validation for ${method}`
);
Object.keys(sequelizeOperators).forEach((operator) => {
t.ifError(
query.validate({ [operator]: true }).error
, `applies sequelize operator "${operator}" in validate.where for ${method}`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query attributeValidation', (t) => {
const { attributeValidation } = t.context;
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method, attributeValidation });
const { query } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query attributeValidation w/ config as plain object', (t) => {
const { attributeValidation } = t.context;
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(config.validate.query),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query attributeValidation w/ config as joi object', (t) => {
const { attributeValidation } = t.context;
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(queryKeys),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query associationValidation', (t) => {
const { attributeValidation, associationValidation, models } = t.context;
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
associationValidation,
});
const { query } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.query when include should be applied`
);
});
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies associationValidation (${key}) to validate.query when include should be applied`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query associationValidation w/ config as plain object', (t) => {
const { associationValidation, models } = t.context;
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
associationValidation,
config,
});
const { query } = configForMethod.validate;
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies ${key} to validate.query`
);
});
Object.keys(config.validate.query).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('query associationValidation w/ config as joi object', (t) => {
const { associationValidation, models } = t.context;
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
includeMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
associationValidation,
config,
});
const { query } = configForMethod.validate;
Object.keys(associationValidation).forEach((key) => {
t.ifError(
query.validate({ [key]: models }).error
, `applies ${key} to validate.query`
);
});
Object.keys(queryKeys).forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.payload associationValidation', (t) => {
const { attributeValidation } = t.context;
payloadMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method, attributeValidation });
const { payload } = configForMethod.validate;
Object.keys(attributeValidation).forEach((key) => {
t.ifError(
payload.validate({ [key]: true }).error
, `applies attributeValidation (${key}) to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('payload attributeValidation w/ config as plain object', (t) => {
const { attributeValidation } = t.context;
const config = {
validate: {
payload: {
aKey: joi.boolean(),
},
},
};
payloadMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { payload } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(config.validate.payload),
];
keys.forEach((key) => {
t.ifError(
payload.validate({ [key]: true }).error
, `applies ${key} to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('payload attributeValidation w/ config as joi object', (t) => {
const { attributeValidation } = t.context;
const payloadKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
payload: joi.object().keys(payloadKeys),
},
};
payloadMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
attributeValidation,
config,
});
const { payload } = configForMethod.validate;
const keys = [
...Object.keys(attributeValidation),
...Object.keys(payloadKeys),
];
keys.forEach((key) => {
t.ifError(
payload.validate({ [key]: true }).error
, `applies ${key} to validate.payload`
);
});
t.truthy(
payload.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.params scopeParamsMethods', (t) => {
const { scopes } = t.context;
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method, scopes });
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
t.truthy(
params.validate({ scope: 'notAthing' }).error
, 'errors on a non-valid key'
);
});
});
test('params scopeParamsMethods w/ config as plain object', (t) => {
const { scopes } = t.context;
const config = {
validate: {
params: {
aKey: joi.boolean(),
},
},
};
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
scopes,
config,
});
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
Object.keys(config.validate.params).forEach((key) => {
t.ifError(
params.validate({ [key]: true }).error
, `applies ${key} to validate.params`
);
});
t.truthy(
params.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('params scopeParamsMethods w/ config as joi object', (t) => {
const { scopes } = t.context;
const paramsKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
params: joi.object().keys(paramsKeys),
},
};
scopeParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
scopes,
config,
});
const { params } = configForMethod.validate;
scopes.forEach((key) => {
t.ifError(
params.validate({ scope: key }).error
, `applies "scope: ${key}" to validate.params`
);
});
Object.keys(paramsKeys).forEach((key) => {
t.ifError(
params.validate({ [key]: true }).error
, `applies ${key} to validate.params`
);
});
t.truthy(
params.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.payload idParamsMethods', (t) => {
idParamsMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { params } = configForMethod.validate;
t.ifError(
params.validate({ id: 'aThing' }).error
, 'applies id to validate.params'
);
});
});
test('validate.query restrictMethods', (t) => {
restrictMethods.forEach((method) => {
const configForMethod = getConfigForMethod({ method });
const { query } = configForMethod.validate;
const restrictKeys = ['limit', 'offset'];
restrictKeys.forEach((key) => {
t.ifError(
query.validate({ [key]: 0 }).error
, `applies restriction (${key}) to validate.query`
);
});
t.ifError(
query.validate({ order: ['thing', 'DESC'] }).error
, 'applies `order` to validate.query'
);
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query restrictMethods w/ config as plain object', (t) => {
const config = {
validate: {
query: {
aKey: joi.boolean(),
},
},
};
restrictMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(config.validate.query),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('validate.query restrictMethods w/ config as joi object', (t) => {
const queryKeys = {
aKey: joi.boolean(),
};
const config = {
validate: {
query: joi.object().keys(queryKeys),
},
};
whereMethods.forEach((method) => {
const configForMethod = getConfigForMethod({
method,
config,
});
const { query } = configForMethod.validate;
const keys = [
...Object.keys(queryKeys),
];
keys.forEach((key) => {
t.ifError(
query.validate({ [key]: true }).error
, `applies ${key} to validate.query`
);
});
t.truthy(
query.validate({ notAThing: true }).error
, 'errors on a non-valid key'
);
});
});
test('does not modify initial config on multiple passes', (t) => {
const { config } = t.context;
const originalConfig = { ...config };
whereMethods.forEach((method) => {
getConfigForMethod({ method, ...t.context });
});
t.deepEqual(
config
, originalConfig
, 'does not modify the original config object'
);
});

View File

@ -1,5 +1,5 @@
if (!global._babelPolyfill) {
require('babel-polyfill');
require('babel/polyfill');
}
import crud, { associations } from './crud';
@ -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,12 +32,11 @@ 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];
@ -93,13 +92,6 @@ 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();
};

View File

@ -1,32 +1,20 @@
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
import { notImplemented } from 'boom';
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
import { omit, identity } from 'lodash';
export const parseInclude = request => {
const include = Array.isArray(request.query.include)
? request.query.include
: [request.query.include]
;
const include = Array.isArray(request.query.include) ? request.query.include
: [request.query.include];
const noGetDb = typeof request.getDb !== 'function';
const noRequestModels = !request.models;
if (noGetDb && noRequestModels) {
return notImplemented('`request.getDb` or `request.models` are not defined.'
return new Error('`request.getDb` or `request.models` are not defined.'
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
}
const { models } = noGetDb ? request : request.getDb();
return include.map(a => {
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
const { _singular, _plural } = models[modelName];
return _singular === a || _plural === a;
});
if (singluarOrPluralMatch) return models[singluarOrPluralMatch];
if (typeof a === 'string') return models[a];
if (a && typeof a.model === 'string' && a.model.length) {
@ -38,7 +26,7 @@ export const parseInclude = request => {
};
export const parseWhere = request => {
const where = omit(request.query, sequelizeKeys);
const where = omit(request.query, 'include');
for (const key of Object.keys(where)) {
try {
@ -51,38 +39,6 @@ export const parseWhere = request => {
return where;
};
export const parseLimitAndOffset = (request) => {
const { limit, offset } = request.query;
const out = {};
if (!isUndefined(limit)) {
out.limit = toNumber(limit);
}
if (!isUndefined(offset)) {
out.offset = toNumber(offset);
}
return out;
};
export const parseOrder = (request) => {
const { order } = request.query;
if (!order) return null;
// 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(' ');
for (const key of Object.keys(order)) {
try {
order[key] = JSON.parse(order[key]);
} catch (e) {
//
}
}
return order;
};
export const getMethod = (model, association, plural = true, method = 'get') => {
const a = plural ? association.original.plural : association.original.singular;
const b = plural ? association.original.singular : association.original.plural; // alternative

View File

@ -1,113 +0,0 @@
import test from 'ava';
import { parseLimitAndOffset, parseOrder, parseWhere } from './utils.js';
test.beforeEach((t) => {
t.context.request = { query: {} };
});
test('parseLimitAndOffset is a function', (t) => {
t.is(typeof parseLimitAndOffset, 'function');
});
test('parseLimitAndOffset returns limit and offset', (t) => {
const { request } = t.context;
request.query.limit = 1;
request.query.offset = 2;
request.query.thing = 'hi';
t.is(
parseLimitAndOffset(request).limit
, request.query.limit
);
t.is(
parseLimitAndOffset(request).offset
, request.query.offset
);
});
test('parseLimitAndOffset returns limit and offset as numbers', (t) => {
const { request } = t.context;
const limit = 1;
const offset = 2;
request.query.limit = `${limit}`;
request.query.offset = `${offset}`;
request.query.thing = 'hi';
t.is(
parseLimitAndOffset(request).limit
, limit
);
t.is(
parseLimitAndOffset(request).offset
, offset
);
});
test('parseOrder is a function', (t) => {
t.is(typeof parseOrder, 'function');
});
test('parseOrder returns order when a string', (t) => {
const { request } = t.context;
const order = 'thing';
request.query.order = order;
request.query.thing = 'hi';
t.deepEqual(
parseOrder(request)
, [order]
);
});
test('parseOrder returns order when json', (t) => {
const { request } = t.context;
const order = [{ model: 'User' }, 'DESC'];
request.query.order = [JSON.stringify({ model: 'User' }), 'DESC'];
request.query.thing = 'hi';
t.deepEqual(
parseOrder(request)
, order
);
});
test('parseOrder returns null when not defined', (t) => {
const { request } = t.context;
request.query.thing = 'hi';
t.is(
parseOrder(request)
, null
);
});
test('parseWhere is a function', (t) => {
t.is(typeof parseWhere, 'function');
});
test('parseWhere returns the non-sequelize keys', (t) => {
const { request } = t.context;
request.query.order = 'thing';
request.query.include = 'User';
request.query.limit = 2;
request.query.thing = 'hi';
t.deepEqual(
parseWhere(request)
, { thing: 'hi' }
);
});
test('parseWhere returns json converted keys', (t) => {
const { request } = t.context;
request.query.order = 'hi';
request.query.thing = '{"id": {"$in": [2, 3]}}';
t.deepEqual(
parseWhere(request)
, { thing: { id: { $in: [2, 3] } } }
);
});

View File

@ -1,18 +0,0 @@
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' },
});
},
},
});
};

View File

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

View File

@ -1,22 +0,0 @@
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' },
});
},
},
});
};

View File

@ -1,70 +0,0 @@
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 });
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 };
};