Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a231f0685 | |||
8cdfc5858d | |||
4e078f5ba5 | |||
85111c7dc8 | |||
196999a4c5 | |||
3e9f024dcf | |||
4c9ae36c5c | |||
4558ad1327 | |||
edccfb2316 | |||
0d8ab9f02e | |||
f062e2b37f | |||
69221ea331 | |||
f33c8da55d | |||
833df49173 | |||
32a539c3d9 | |||
b35bd23c91 | |||
b4ea8c5b8e | |||
85cd2823da | |||
e0132c2cae | |||
bd18c57529 | |||
3e53ba8d2c | |||
81b704a395 | |||
d69b87b8fa | |||
03755f94c5 | |||
7cecd7fb40 | |||
7b757fcc50 | |||
de0685c8bb | |||
f2f613b35b | |||
38ccb3adf6 | |||
506d42f39a | |||
3dfa72ddee | |||
bab2e90cbb | |||
da6b3ce963 | |||
b032be20d1 | |||
79c6a81a3a | |||
517f2b8157 | |||
a9fa790ae9 | |||
ce6f1fedde | |||
db86507ef9 | |||
6ad9df2db1 | |||
17105f66f4 | |||
b18479e02e | |||
0e9cd935b9 | |||
9524e55690 | |||
1752d700f5 | |||
6d289d6d78 | |||
0d6a715511 | |||
e5d72fd034 | |||
a0aeaef3a9 | |||
79b9fc1242 | |||
fb8275abca | |||
098aabfea5 | |||
f95f411a65 | |||
0416986896 | |||
6c46ff68d0 | |||
29ee49fc62 | |||
d142e6c553 | |||
fb06d9cd27 | |||
b9e150200f | |||
fe6881099f | |||
6786b9b487 | |||
aa06808ab2 | |||
2d1ab6b75a | |||
2ea6c2e3a8 | |||
c0cb2c44e0 | |||
e7bc048a46 |
13
.babelrc
13
.babelrc
@ -1,3 +1,14 @@
|
||||
{
|
||||
"stage": 1
|
||||
"presets": [
|
||||
"stage-1"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties",
|
||||
"add-module-exports",
|
||||
"closure-elimination",
|
||||
"transform-decorators-legacy",
|
||||
"transform-es2015-modules-commonjs"
|
||||
],
|
||||
"sourceMaps": true
|
||||
}
|
||||
|
@ -1,3 +1,9 @@
|
||||
{
|
||||
"extends": "pichak"
|
||||
"plugins": [
|
||||
"ava"
|
||||
],
|
||||
"extends": [
|
||||
"pichak",
|
||||
"plugin:ava/recommended"
|
||||
]
|
||||
}
|
||||
|
7
CONTRIBUTING
Normal file
7
CONTRIBUTING
Normal file
@ -0,0 +1,7 @@
|
||||
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).
|
29
Gruntfile.js
29
Gruntfile.js
@ -1,29 +0,0 @@
|
||||
module.exports = function(grunt) {
|
||||
grunt.initConfig({
|
||||
babel: {
|
||||
scripts: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'src',
|
||||
src: '**/*.js',
|
||||
dest: 'build/'
|
||||
}]
|
||||
}
|
||||
},
|
||||
clean: {
|
||||
files: ['build/**/*.js']
|
||||
},
|
||||
watch: {
|
||||
scripts: {
|
||||
files: ['src/**/*.js', 'server/**/*.js'],
|
||||
tasks: ['babel']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-babel');
|
||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
|
||||
grunt.registerTask('default', ['clean', 'babel']);
|
||||
};
|
87
README.md
87
README.md
@ -1,4 +1,4 @@
|
||||
hapi-sequelize-crud
|
||||
hapi-sequelize-crud [](https://circleci.com/gh/mdibaiee/hapi-sequelize-crud)
|
||||
===================
|
||||
|
||||
Automatically generate a RESTful API for your models and associations
|
||||
@ -11,6 +11,9 @@ 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({
|
||||
@ -28,15 +31,86 @@ await register({
|
||||
register: require('hapi-sequelize-crud'),
|
||||
options: {
|
||||
prefix: '/v1',
|
||||
defaultConfig: { ... } // passed as `config` to all routes created
|
||||
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: [
|
||||
// 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']}
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Please note that you should register `hapi-sequelize-crud` after defining your
|
||||
associations.
|
||||
### 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
|
||||
|
||||
##What do I get
|
||||
## `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]})
|
||||
```
|
||||
|
||||
## Full list of methods
|
||||
|
||||
Let's say you have a `many-to-many` association like this:
|
||||
|
||||
@ -51,8 +125,9 @@ You get these:
|
||||
# get an array of records
|
||||
GET /team/{id}/roles
|
||||
GET /role/{id}/teams
|
||||
# might also append query parameters to search for
|
||||
# might also append `where` 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}
|
||||
|
9
circle.yml
Normal file
9
circle.yml
Normal file
@ -0,0 +1,9 @@
|
||||
machine:
|
||||
node:
|
||||
version: 6.5.0
|
||||
|
||||
dependencies:
|
||||
pre:
|
||||
- npm prune
|
||||
post:
|
||||
- mkdir -p $CIRCLE_TEST_REPORTS/ava
|
44
package.json
44
package.json
@ -1,16 +1,19 @@
|
||||
{
|
||||
"name": "hapi-sequelize-crud",
|
||||
"version": "2.1.0",
|
||||
"version": "2.6.1",
|
||||
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
||||
"main": "build/index.js",
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"pre-commit": "npm run lint && grunt"
|
||||
"pre-commit": "npm run lint && npm run build"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src test",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"lint": "eslint src",
|
||||
"test": "ava --require babel-register --source='*.test.js' --tap=${CI-false} | $(if [ -z ${CI:-} ]; then echo 'tail'; else tap-xunit > $CIRCLE_TEST_REPORTS/ava/ava.xml; fi;)",
|
||||
"tdd": "ava --require babel-register --source='*.test.js' --watch",
|
||||
"build": "scripty",
|
||||
"watch": "scripty"
|
||||
},
|
||||
"repository": {
|
||||
"git": "https://github.com/mdibaiee/hapi-sequelize-crud"
|
||||
@ -21,19 +24,28 @@
|
||||
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"babel": "5.8.3",
|
||||
"eslint": "2.10.2",
|
||||
"eslint-config-pichak": "1.0.1",
|
||||
"ghooks": "1.0.3",
|
||||
"grunt": "0.4.5",
|
||||
"grunt-babel": "5.0.3",
|
||||
"grunt-contrib-clean": "0.7.0",
|
||||
"grunt-contrib-watch": "0.6.1"
|
||||
"ava": "^0.16.0",
|
||||
"babel-cli": "^6.14.0",
|
||||
"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.14.0",
|
||||
"babel-preset-stage-1": "^6.13.0",
|
||||
"eslint": "^3.4.0",
|
||||
"eslint-config-pichak": "^1.1.2",
|
||||
"eslint-plugin-ava": "^3.0.0",
|
||||
"ghooks": "^1.3.2",
|
||||
"scripty": "^1.6.0",
|
||||
"sinon": "^1.17.5",
|
||||
"sinon-bluebird": "^3.0.2",
|
||||
"tap-xunit": "^1.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel": "5.8.3",
|
||||
"boom": "^3.2.2",
|
||||
"joi": "7.2.1",
|
||||
"lodash": "4.0.0"
|
||||
"boom": "^4.0.0",
|
||||
"joi": "^9.0.4",
|
||||
"lodash": "^4.15.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"babel-polyfill": "^6.13.0"
|
||||
}
|
||||
}
|
||||
|
15
scripts/build.sh
Executable file
15
scripts/build.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
|
||||
source "scripts/env.sh"
|
||||
|
||||
babel="./node_modules/.bin/babel"
|
||||
|
||||
build () {
|
||||
$babel "$SRC_DIR" --out-dir "$OUT_DIR" $@
|
||||
}
|
||||
|
||||
build $@
|
7
scripts/env.sh
Normal file
7
scripts/env.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
export SRC_DIR="./src"
|
||||
export OUT_DIR="./build"
|
6
scripts/watch.sh
Executable file
6
scripts/watch.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
./scripts/build.sh --watch
|
@ -14,15 +14,15 @@ export default (server, a, b, names, options) => {
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
let instanceb = await b.findOne({
|
||||
const instanceb = await b.findOne({
|
||||
where: {
|
||||
id: request.params.bid,
|
||||
[b.primaryKeyField]: request.params.bid,
|
||||
},
|
||||
});
|
||||
|
||||
let instancea = await a.findOne({
|
||||
const instancea = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -28,18 +28,23 @@ export const get = (server, a, b, names) => {
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
|
||||
const base = a.findOne({
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
const method = getMethod(base, names.b);
|
||||
|
||||
const list = await method({ where: {
|
||||
id: request.params.bid,
|
||||
[b.primaryKeyField]: request.params.bid,
|
||||
}, include });
|
||||
|
||||
reply(list);
|
||||
if (Array.isArray(list)) {
|
||||
reply(list[0]);
|
||||
} else {
|
||||
reply(list);
|
||||
}
|
||||
},
|
||||
|
||||
config: defaultConfig,
|
||||
@ -58,7 +63,7 @@ export const list = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
@ -73,7 +78,7 @@ export const list = (server, a, b, names) => {
|
||||
};
|
||||
|
||||
export const scope = (server, a, b, names) => {
|
||||
let scopes = Object.keys(b.options.scopes);
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
@ -86,7 +91,7 @@ export const scope = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
@ -112,7 +117,7 @@ export const scope = (server, a, b, names) => {
|
||||
};
|
||||
|
||||
export const scopeScope = (server, a, b, names) => {
|
||||
let scopes = {
|
||||
const scopes = {
|
||||
a: Object.keys(a.options.scopes),
|
||||
b: Object.keys(b.options.scopes),
|
||||
};
|
||||
@ -126,7 +131,7 @@ export const scopeScope = (server, a, b, names) => {
|
||||
const include = parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
let list = await b.scope(request.params.scopeb).findAll({
|
||||
const list = await b.scope(request.params.scopeb).findAll({
|
||||
where,
|
||||
include: include.concat({
|
||||
model: a.scope(request.params.scopea),
|
||||
@ -159,7 +164,7 @@ export const destroy = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
@ -175,7 +180,7 @@ export const destroy = (server, a, b, names) => {
|
||||
};
|
||||
|
||||
export const destroyScope = (server, a, b, names) => {
|
||||
let scopes = Object.keys(b.options.scopes);
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
@ -188,7 +193,7 @@ export const destroyScope = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primarykeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
@ -228,7 +233,7 @@ export const update = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -26,7 +26,7 @@ export const get = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
const method = getMethod(base, names.b, false);
|
||||
@ -53,7 +53,7 @@ export const create = (server, a, b, names) => {
|
||||
async handler(request, reply) {
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.id,
|
||||
[a.primaryKeyField]: request.params.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -79,10 +79,12 @@ export const destroy = (server, a, b, names) => {
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
id: request.params.aid,
|
||||
[a.primaryKeyField]: request.params.aid,
|
||||
},
|
||||
});
|
||||
|
||||
where[b.primaryKeyField] = request.params.bid;
|
||||
|
||||
const method = getMethod(base, names.b, false, 'get');
|
||||
const instance = await method({ where, include });
|
||||
await instance.destroy();
|
||||
@ -110,6 +112,8 @@ export const update = (server, a, b, names) => {
|
||||
},
|
||||
});
|
||||
|
||||
where[b.primaryKeyField] = request.params.bid;
|
||||
|
||||
const method = getMethod(base, names.b, false);
|
||||
|
||||
const instance = await method({ where, include });
|
||||
|
259
src/crud.js
259
src/crud.js
@ -1,142 +1,255 @@
|
||||
import joi from 'joi';
|
||||
import path from 'path';
|
||||
import error from './error';
|
||||
import _ from 'lodash';
|
||||
import { parseInclude, parseWhere } from './utils';
|
||||
import { notFound } from 'boom';
|
||||
import * as associations from './associations/index';
|
||||
import getConfigForMethod from './get-config-for-method.js';
|
||||
|
||||
let prefix;
|
||||
let defaultConfig;
|
||||
|
||||
export default (server, model, options) => {
|
||||
prefix = options.prefix;
|
||||
defaultConfig = options.defaultConfig;
|
||||
|
||||
list(server, model);
|
||||
get(server, model);
|
||||
scope(server, model);
|
||||
create(server, model);
|
||||
destroy(server, model);
|
||||
destroyAll(server, model);
|
||||
destroyScope(server, model);
|
||||
update(server, model);
|
||||
const createAll = ({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
}) => {
|
||||
Object.keys(methods).forEach((method) => {
|
||||
methods[method]({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config: getConfigForMethod({
|
||||
method,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
config,
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const list = (server, model) => {
|
||||
export { associations };
|
||||
|
||||
/*
|
||||
The `models` option, becomes `permissions`, and can look like:
|
||||
|
||||
```
|
||||
models: ['cat', 'dog']
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
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 attributeValidation = modelAttributes.reduce((params, attribute) => {
|
||||
params[attribute] = joi.any();
|
||||
return params;
|
||||
}, {});
|
||||
|
||||
const associationValidation = {
|
||||
include: joi.array().items(joi.string().valid(...modelAssociations)),
|
||||
};
|
||||
|
||||
// if we don't have any permissions set, just create all the methods
|
||||
if (!permissions) {
|
||||
createAll({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
// if we've gotten here, we have complex permissions and need to set them
|
||||
} else {
|
||||
const permissionOptions = permissions.filter((permission) => {
|
||||
return permission.model === modelName;
|
||||
});
|
||||
|
||||
permissionOptions.forEach((permissionOption) => {
|
||||
if (_.isPlainObject(permissionOption)) {
|
||||
const permissionConfig = permissionOption.config || config;
|
||||
|
||||
if (permissionOption.methods) {
|
||||
permissionOption.methods.forEach((method) => {
|
||||
methods[method]({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config: getConfigForMethod({
|
||||
method,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
config: permissionConfig,
|
||||
}),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
createAll({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
config: permissionConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const list = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${model._plural}`,
|
||||
path: path.join(prefix, model._plural),
|
||||
|
||||
@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.findAll({
|
||||
where, include,
|
||||
});
|
||||
|
||||
reply(list);
|
||||
reply(list.map((item) => item.toJSON()));
|
||||
},
|
||||
|
||||
config: defaultConfig,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
export const get = (server, model) => {
|
||||
export const get = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${model._singular}/{id?}`,
|
||||
path: path.join(prefix, model._singular, '{id?}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const {id} = request.params;
|
||||
if (id) where.id = id;
|
||||
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);
|
||||
reply(instance.toJSON());
|
||||
},
|
||||
config: _.defaultsDeep({
|
||||
config: _.defaultsDeep(config, {
|
||||
validate: {
|
||||
params: joi.object().keys({
|
||||
params: {
|
||||
id: joi.any(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}, defaultConfig),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const scope = (server, model) => {
|
||||
let scopes = Object.keys(model.options.scopes);
|
||||
export const scope = ({ server, model, prefix = '/', config }) => {
|
||||
const scopes = Object.keys(model.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${model._plural}/{scope}`,
|
||||
path: path.join(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 });
|
||||
|
||||
reply(list);
|
||||
reply(list.map((item) => item.toJSON()));
|
||||
},
|
||||
config: _.defaultsDeep({
|
||||
config: _.defaultsDeep(config, {
|
||||
validate: {
|
||||
params: joi.object().keys({
|
||||
params: {
|
||||
scope: joi.string().valid(...scopes),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}, defaultConfig),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const create = (server, model) => {
|
||||
export const create = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${prefix}/${model._singular}`,
|
||||
path: path.join(prefix, model._singular),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const instance = await model.create(request.payload);
|
||||
|
||||
reply(instance);
|
||||
reply(instance.toJSON());
|
||||
},
|
||||
|
||||
config: defaultConfig,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (server, model) => {
|
||||
export const destroy = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${model._singular}/{id?}`,
|
||||
path: path.join(prefix, model._singular, '{id?}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const where = parseWhere(request);
|
||||
if (request.params.id) where.id = request.params.id;
|
||||
if (request.params.id) where[model.primaryKeyField] = request.params.id;
|
||||
|
||||
const list = await model.findAll({ where });
|
||||
|
||||
await Promise.all(list.map(instance => instance.destroy()));
|
||||
|
||||
reply(list.length === 1 ? list[0] : list);
|
||||
const listAsJSON = list.map((item) => item.toJSON());
|
||||
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
|
||||
},
|
||||
|
||||
config: defaultConfig,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyAll = (server, model) => {
|
||||
export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${model._plural}`,
|
||||
path: path.join(prefix, model._plural),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
@ -146,69 +259,73 @@ export const destroyAll = (server, model) => {
|
||||
|
||||
await Promise.all(list.map(instance => instance.destroy()));
|
||||
|
||||
reply(list.length === 1 ? list[0] : list);
|
||||
const listAsJSON = list.map((item) => item.toJSON());
|
||||
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
|
||||
},
|
||||
|
||||
config: defaultConfig,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyScope = (server, model) => {
|
||||
let scopes = Object.keys(model.options.scopes);
|
||||
export const destroyScope = ({ server, model, prefix = '/', config }) => {
|
||||
const scopes = Object.keys(model.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${model._plural}/{scope}`,
|
||||
path: path.join(prefix, model._plural, '{scope}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
let list = await model.scope(request.params.scope).findAll({ include, where });
|
||||
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()));
|
||||
|
||||
reply(list);
|
||||
const listAsJSON = list.map((item) => item.toJSON());
|
||||
reply(listAsJSON.length === 1 ? listAsJSON[0] : listAsJSON);
|
||||
},
|
||||
config: _.defaultsDeep({
|
||||
config: _.defaultsDeep(config, {
|
||||
validate: {
|
||||
params: joi.object().keys({
|
||||
params: {
|
||||
scope: joi.string().valid(...scopes),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}, defaultConfig),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const update = (server, model) => {
|
||||
export const update = ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${prefix}/${model._singular}/{id}`,
|
||||
path: path.join(prefix, model._singular, '{id}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const {id} = request.params;
|
||||
const instance = await model.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
const { id } = request.params;
|
||||
const instance = await model.findById(id);
|
||||
|
||||
if (!instance) return void reply(notFound(`${id} not found.`));
|
||||
|
||||
await instance.update(request.payload);
|
||||
|
||||
reply(instance);
|
||||
reply(instance.toJSON());
|
||||
},
|
||||
|
||||
config: _.defaultsDeep({
|
||||
config: _.defaultsDeep(config, {
|
||||
validate: {
|
||||
payload: joi.object().required(),
|
||||
params: {
|
||||
id: joi.any(),
|
||||
},
|
||||
},
|
||||
}, defaultConfig),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
import * as associations from './associations/index';
|
||||
export { associations };
|
||||
const methods = {
|
||||
list, get, scope, create, destroy, destroyAll, destroyScope, update,
|
||||
};
|
||||
|
157
src/crud.test.js
Normal file
157
src/crud.test.js
Normal file
@ -0,0 +1,157 @@
|
||||
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.model],
|
||||
};
|
||||
});
|
||||
|
||||
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.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'
|
||||
);
|
||||
});
|
61
src/error.js
61
src/error.js
@ -1,12 +1,65 @@
|
||||
import Boom from 'boom';
|
||||
|
||||
export default (target, key, descriptor) => {
|
||||
let fn = descriptor.value;
|
||||
const fn = descriptor.value;
|
||||
|
||||
descriptor.value = async (request, reply) => {
|
||||
try {
|
||||
await fn(request, reply);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
reply(e);
|
||||
} 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 {
|
||||
reply(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
88
src/get-config-for-method.js
Normal file
88
src/get-config-for-method.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import joi from 'joi';
|
||||
|
||||
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 default ({ method, attributeValidation, associationValidation, config = {} }) => {
|
||||
const hasWhere = whereMethods.includes(method);
|
||||
const hasInclude = includeMethods.includes(method);
|
||||
const hasPayload = payloadMethods.includes(method);
|
||||
const methodConfig = { ...config };
|
||||
|
||||
if (hasWhere) {
|
||||
defaultsDeep(methodConfig, {
|
||||
validate: {
|
||||
query: {
|
||||
...attributeValidation,
|
||||
...sequelizeOperators,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (hasInclude) {
|
||||
defaultsDeep(methodConfig, {
|
||||
validate: {
|
||||
query: {
|
||||
...associationValidation,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPayload) {
|
||||
defaultsDeep(methodConfig, {
|
||||
validate: {
|
||||
payload: {
|
||||
...attributeValidation,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return methodConfig;
|
||||
};
|
117
src/get-config-for-method.test.js
Normal file
117
src/get-config-for-method.test.js
Normal file
@ -0,0 +1,117 @@
|
||||
import test from 'ava';
|
||||
import joi from 'joi';
|
||||
import
|
||||
getConfigForMethod, {
|
||||
whereMethods,
|
||||
includeMethods,
|
||||
payloadMethods,
|
||||
sequelizeOperators,
|
||||
} from './get-config-for-method.js';
|
||||
|
||||
test.beforeEach((t) => {
|
||||
t.context.attributeValidation = {
|
||||
myKey: joi.any(),
|
||||
};
|
||||
|
||||
t.context.associationValidation = {
|
||||
include: ['MyModel'],
|
||||
};
|
||||
|
||||
t.context.config = {
|
||||
cors: {},
|
||||
};
|
||||
});
|
||||
|
||||
test('get-config-for-method validate.query seqeulizeOperators', (t) => {
|
||||
whereMethods.forEach((method) => {
|
||||
const configForMethod = getConfigForMethod({ method });
|
||||
const { query } = configForMethod.validate;
|
||||
const configForMethodValidateQueryKeys = Object.keys(query);
|
||||
|
||||
t.truthy(
|
||||
query,
|
||||
`applies query validation for ${method}`
|
||||
);
|
||||
|
||||
Object.keys(sequelizeOperators).forEach((operator) => {
|
||||
t.truthy(
|
||||
configForMethodValidateQueryKeys.includes(operator),
|
||||
`applies sequelize operator "${operator}" in validate.where for ${method}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('get-config-for-method 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.truthy(
|
||||
query[key]
|
||||
, `applies attributeValidation (${key}) to validate.query`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('get-config-for-method validate.query associationValidation', (t) => {
|
||||
const { attributeValidation, associationValidation } = t.context;
|
||||
|
||||
includeMethods.forEach((method) => {
|
||||
const configForMethod = getConfigForMethod({
|
||||
method,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
});
|
||||
const { query } = configForMethod.validate;
|
||||
|
||||
Object.keys(attributeValidation).forEach((key) => {
|
||||
t.truthy(
|
||||
query[key]
|
||||
, `applies attributeValidation (${key}) to validate.query when include should be applied`
|
||||
);
|
||||
});
|
||||
|
||||
Object.keys(associationValidation).forEach((key) => {
|
||||
t.truthy(
|
||||
query[key]
|
||||
, `applies associationValidation (${key}) to validate.query when include should be applied`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('get-config-for-method 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.truthy(
|
||||
payload[key]
|
||||
, `applies attributeValidation (${key}) to validate.payload`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('get-config-for-method does not modify initial config on multiple passes', (t) => {
|
||||
const { config } = t.context;
|
||||
const originalConfig = { ...config };
|
||||
|
||||
whereMethods.forEach((method) => {
|
||||
getConfigForMethod({ method, config });
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
config
|
||||
, originalConfig
|
||||
, 'does not modify the original config object'
|
||||
);
|
||||
});
|
46
src/index.js
46
src/index.js
@ -1,5 +1,5 @@
|
||||
if (!global._babelPolyfill) {
|
||||
require('babel/polyfill');
|
||||
require('babel-polyfill');
|
||||
}
|
||||
|
||||
import crud, { associations } from './crud';
|
||||
@ -8,9 +8,10 @@ import qs from 'qs';
|
||||
|
||||
const register = (server, options = {}, next) => {
|
||||
options.prefix = options.prefix || '';
|
||||
options.name = options.name || 'db';
|
||||
|
||||
let db = server.plugins['hapi-sequelize'].db;
|
||||
let models = db.sequelize.models;
|
||||
const db = server.plugins['hapi-sequelize'][options.name];
|
||||
const models = db.sequelize.models;
|
||||
|
||||
const onRequest = function (request, reply) {
|
||||
const uri = request.raw.req.url;
|
||||
@ -26,9 +27,9 @@ const register = (server, options = {}, next) => {
|
||||
method: onRequest,
|
||||
});
|
||||
|
||||
for (let modelName of Object.keys(models)) {
|
||||
let model = models[modelName];
|
||||
let { plural, singular } = model.options.name;
|
||||
for (const modelName of Object.keys(models)) {
|
||||
const model = models[modelName];
|
||||
const { plural, singular } = model.options.name;
|
||||
model._plural = plural.toLowerCase();
|
||||
model._singular = singular.toLowerCase();
|
||||
|
||||
@ -37,29 +38,30 @@ const register = (server, options = {}, next) => {
|
||||
|
||||
crud(server, model, options);
|
||||
|
||||
for (let key of Object.keys(model.associations)) {
|
||||
let association = model.associations[key];
|
||||
let { source, target } = association;
|
||||
for (const key of Object.keys(model.associations)) {
|
||||
const association = model.associations[key];
|
||||
const { source, target } = association;
|
||||
|
||||
let sourceName = source.options.name;
|
||||
const sourceName = source.options.name;
|
||||
|
||||
const names = (rev) => {
|
||||
const arr = [{
|
||||
plural: sourceName.plural.toLowerCase(),
|
||||
singular: sourceName.singular.toLowerCase(),
|
||||
original: sourceName,
|
||||
}, {
|
||||
plural: association.options.name.plural.toLowerCase(),
|
||||
singular: association.options.name.singular.toLowerCase(),
|
||||
original: association.options.name,
|
||||
}];
|
||||
plural: sourceName.plural.toLowerCase(),
|
||||
singular: sourceName.singular.toLowerCase(),
|
||||
original: sourceName,
|
||||
}, {
|
||||
plural: association.options.name.plural.toLowerCase(),
|
||||
singular: association.options.name.singular.toLowerCase(),
|
||||
original: association.options.name,
|
||||
}];
|
||||
|
||||
return rev ? { b: arr[0], a: arr[1] } : { a: arr[0], b: arr[1] };
|
||||
};
|
||||
|
||||
let targetAssociations = target.associations[sourceName.plural] || target.associations[sourceName.singular];
|
||||
let sourceType = association.associationType,
|
||||
targetType = (targetAssociations || {}).associationType;
|
||||
const targetAssociations = target.associations[sourceName.plural]
|
||||
|| target.associations[sourceName.singular];
|
||||
const sourceType = association.associationType,
|
||||
targetType = (targetAssociations || {}).associationType;
|
||||
|
||||
try {
|
||||
if (sourceType === 'BelongsTo' && (targetType === 'BelongsTo' || !targetType)) {
|
||||
@ -83,7 +85,7 @@ const register = (server, options = {}, next) => {
|
||||
|
||||
associations.associate(server, source, target, names(), options);
|
||||
associations.associate(server, target, source, names(1), options);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// There might be conflicts in case of models associated with themselves and some other
|
||||
// rare cases.
|
||||
}
|
||||
|
19
src/utils.js
19
src/utils.js
@ -1,18 +1,29 @@
|
||||
import { omit } from 'lodash';
|
||||
import { omit, identity } from 'lodash';
|
||||
import { notImplemented } from 'boom';
|
||||
|
||||
export const parseInclude = request => {
|
||||
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.'
|
||||
+ '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 request.models[a];
|
||||
if (typeof a === 'string') return models[a];
|
||||
|
||||
if (a && typeof a.model === 'string' && a.model.length) {
|
||||
a.model = request.models[a.model];
|
||||
a.model = models[a.model];
|
||||
}
|
||||
|
||||
return a;
|
||||
}).filter(a => a);
|
||||
}).filter(identity);
|
||||
};
|
||||
|
||||
export const parseWhere = request => {
|
||||
|
Reference in New Issue
Block a user