20 Commits

Author SHA1 Message Date
823a65991a 2.5.1 2016-07-22 11:28:47 -07:00
f9b997b65c Change: permissions must always be an array
This allows us to set different configs per model. I should have thought
of this usecase when I first did permissions.
2016-07-22 11:28:25 -07:00
7c2b146eed 2.5.0 2016-07-21 18:02:09 -07:00
a54683e29a 2.4.0 2016-07-21 18:02:04 -07:00
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
6c46ff68d0 fix(primaryKey): use model.primaryKey instead of hardcoded id for id routes, fixes #9 2016-07-13 11:27:48 +04:30
29ee49fc62 fix(name): default name parameter on options object itself 2016-07-13 10:05:33 +04:30
d142e6c553 fix(options): take a name parameter, same as the name parameter used in hapi-sequelize, fixes #8 2016-07-13 10:03:00 +04:30
fb06d9cd27 chore(version): bump version up 2016-07-13 09:59:55 +04:30
b9e150200f chore: switch from Gruntfiles to npm scripts, see #5 2016-07-13 09:59:12 +04:30
fe6881099f Merge branch 'babel6' of github.com:Getable/hapi-sequelize-crud into babel6 2016-07-13 09:50:44 +04:30
6786b9b487 style: updated eslint config 2016-07-09 09:41:23 +04:30
aa06808ab2 chore(version): bump version to 2.1.1, two small patches were applied by @joeybaker 2016-07-09 09:14:18 +04:30
2d1ab6b75a Merge pull request #6 from Getable/fix-include
Fix: include option api correction
2016-07-09 09:13:14 +04:30
2ea6c2e3a8 Fix: include option api correction
AFAIK, hapi-sequelize doesn't have a `request.models`, but it does have
a `request.getDb()` method that has `models` on it. This calls that
method to get the related models, but allows `request.models` to keep
working for backward compatibility.
2016-07-08 15:28:33 -07:00
c0cb2c44e0 Merge pull request #7 from Getable/require-payload-on-put
Fix: PUT requires a payload to work
2016-07-08 17:17:40 +04:30
e7bc048a46 Internal: upgrade to babel6
Upgrades to babel 6. The major win (aside from some minor
performance gains), is that this module can now be `npm link`ed into a
babel 6 code base.
2016-07-05 14:47:44 -07:00
14 changed files with 240 additions and 127 deletions

View File

@ -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
}

View File

@ -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']);
};

View File

@ -28,11 +28,42 @@ 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
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']}
]
models: {
bat: {
}
}
}
});
```
### 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
Please note that you should register `hapi-sequelize-crud` after defining your
associations.

View File

@ -1,19 +1,21 @@
{
"name": "hapi-sequelize-crud",
"version": "2.1.0",
"name": "@getable/hapi-sequelize-crud",
"version": "2.5.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"
"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"
@ -21,14 +23,16 @@
"author": "Mahdi Dibaiee <mdibaiee@aol.com> (http://dibaiee.ir/)",
"license": "MIT",
"devDependencies": {
"babel": "5.8.3",
"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.10.3",
"babel-preset-stage-1": "^6.5.0",
"eslint": "2.10.2",
"eslint-config-pichak": "1.0.1",
"eslint-config-pichak": "1.1.0",
"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"
"scripty": "^1.6.0"
},
"dependencies": {
"babel": "5.8.3",

15
scripts/build.sh Executable file
View 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
View 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
View 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

View File

@ -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,
},
});

View File

@ -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,
},
});

View File

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

View File

@ -4,24 +4,65 @@ import _ from 'lodash';
import { parseInclude, parseWhere } from './utils';
import { notFound } from 'boom';
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 }) => {
Object.keys(methods).forEach((method) => {
methods[method]({ server, model, prefix, config });
});
};
export const list = (server, model) => {
/*
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 permissionsIsArray = Array.isArray(permissions);
if (!permissions) {
createAll({ server, model, prefix, config });
} else if (permissionsIsArray && permissions.includes(modelName)) {
createAll({ server, model, prefix, config });
} else if (permissionsIsArray) {
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: permissionConfig,
});
});
} else {
createAll({ server, model, prefix, config: permissionConfig });
}
}
});
}
};
export const list = ({ server, model, prefix, config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._plural}`,
@ -31,6 +72,8 @@ export const list = (server, model) => {
const include = parseInclude(request);
const where = parseWhere(request);
if (include instanceof Error) return void reply(include);
const list = await model.findAll({
where, include,
});
@ -38,11 +81,11 @@ export const list = (server, model) => {
reply(list);
},
config: defaultConfig,
config,
});
};
export const get = (server, model) => {
export const get = ({ server, model, prefix, config }) => {
server.route({
method: 'GET',
path: `${prefix}/${model._singular}/{id?}`,
@ -51,8 +94,8 @@ export const get = (server, model) => {
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;
const instance = await model.findOne({ where, include });
@ -66,12 +109,12 @@ export const get = (server, model) => {
id: joi.any(),
}),
},
}, defaultConfig),
}, config),
});
};
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',
@ -92,11 +135,11 @@ export const scope = (server, model) => {
scope: joi.string().valid(...scopes),
}),
},
}, defaultConfig),
}, config),
});
};
export const create = (server, model) => {
export const create = ({ server, model, prefix, config }) => {
server.route({
method: 'POST',
path: `${prefix}/${model._singular}`,
@ -108,11 +151,11 @@ export const create = (server, model) => {
reply(instance);
},
config: defaultConfig,
config,
});
};
export const destroy = (server, model) => {
export const destroy = ({ server, model, prefix, config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._singular}/{id?}`,
@ -120,7 +163,7 @@ export const destroy = (server, model) => {
@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 });
@ -129,11 +172,11 @@ export const destroy = (server, model) => {
reply(list.length === 1 ? list[0] : list);
},
config: defaultConfig,
config,
});
};
export const destroyAll = (server, model) => {
export const destroyAll = ({ server, model, prefix, config }) => {
server.route({
method: 'DELETE',
path: `${prefix}/${model._plural}`,
@ -149,12 +192,12 @@ export const destroyAll = (server, model) => {
reply(list.length === 1 ? list[0] : list);
},
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',
@ -165,7 +208,7 @@ export const destroyScope = (server, model) => {
const include = parseInclude(request);
const where = parseWhere(request);
let list = await model.scope(request.params.scope).findAll({ include, where });
const list = await model.scope(request.params.scope).findAll({ include, where });
await Promise.all(list.map(instance => instance.destroy()));
@ -177,18 +220,18 @@ export const destroyScope = (server, model) => {
scope: joi.string().valid(...scopes),
}),
},
}, defaultConfig),
}, config),
});
};
export const update = (server, model) => {
export const update = ({ server, model, prefix, config }) => {
server.route({
method: 'PUT',
path: `${prefix}/${model._singular}/{id}`,
@error
async handler(request, reply) {
const {id} = request.params;
const { id } = request.params;
const instance = await model.findOne({
where: {
id,
@ -206,9 +249,13 @@ export const update = (server, model) => {
validate: {
payload: joi.object().required(),
},
}, defaultConfig),
}, config),
});
};
import * as associations from './associations/index';
export { associations };
const methods = {
list, get, scope, create, destroy, destroyAll, destroyScope, update,
};

View File

@ -1,10 +1,10 @@
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) {
} catch (e) {
console.error(e);
reply(e);
}

View File

@ -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.
}

View File

@ -1,18 +1,28 @@
import { omit } from 'lodash';
import { omit, identity } from 'lodash';
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 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 => {
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 => {