Compare commits
8 Commits
master
...
labibramad
Author | SHA1 | Date | |
---|---|---|---|
|
05793eb749 | ||
|
e632f79e2b | ||
|
6fa9e90ec5 | ||
|
49d24ea265 | ||
|
11306667d6 | ||
|
1977304287 | ||
|
a141a38fe5 | ||
|
72452a0088 |
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 The hapi-sequelize-crud Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
59
README.md
59
README.md
@ -109,10 +109,17 @@ Getting related models is easy, just use a query parameter `include`.
|
||||
|
||||
```js
|
||||
// returns all teams with their related City model
|
||||
// GET /teams?include=city
|
||||
// GET /teams?include=city or
|
||||
// GET /teams?include={"model": "City"}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: City})
|
||||
|
||||
or if association defined with an alias
|
||||
// GET /players?include={"model": "Master", "as": "Couch"}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Players.findAll({include: Master, as: 'Couch'})
|
||||
```
|
||||
|
||||
If you want to get multiple related models, just pass multiple `include` parameters.
|
||||
@ -133,6 +140,56 @@ For models that have a many-to-many relationship, you can also pass the plural v
|
||||
Team.findAll({include: [Player]})
|
||||
```
|
||||
|
||||
Filtering by related models property, you can pass **where** paremeter inside each **include** items object.
|
||||
```js
|
||||
// returns all team with their related City where City property name equals Healdsburg
|
||||
// GET /teams?include={"model": "City", "where": {"name": "Healdsburg"}}
|
||||
|
||||
// results in a Sequelize query:
|
||||
Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}})
|
||||
```
|
||||
|
||||
More complex example with nested include, association alias and association filtering.
|
||||
```js
|
||||
// returns all team with its players along with its couch of each player
|
||||
// GET /cities?include[]={
|
||||
// "model": "Team",
|
||||
// "include": {
|
||||
// "model": "Player",
|
||||
// "where": {
|
||||
// "name": "Pinot"
|
||||
// },
|
||||
// "include": {
|
||||
// "model": "Master",
|
||||
// "as": "Coach",
|
||||
// "where": {
|
||||
// "name": "Shifu"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// results in a Sequelize query:
|
||||
City.findAll({
|
||||
include: {
|
||||
model: Team,
|
||||
include: {
|
||||
model: Player,
|
||||
where: {
|
||||
name: Pinot
|
||||
},
|
||||
include: {
|
||||
model: Master,
|
||||
as: 'Coach',
|
||||
where: {
|
||||
name: 'Shifu'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## `limit` and `offset` queries
|
||||
Restricting list (`GET`) and scope queries to a restricted count can be done by passing `limit=<number>` and/or `offset=<number>`.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hapi-sequelize-crud",
|
||||
"version": "2.9.3",
|
||||
"version": "2.9.1",
|
||||
"description": "Hapi plugin that automatically generates RESTful API for CRUD",
|
||||
"main": "build/index.js",
|
||||
"config": {
|
||||
|
@ -19,14 +19,14 @@ export default (server, a, b, names, options) => {
|
||||
update(server, a, b, names);
|
||||
};
|
||||
|
||||
export const get = (server, a, b, names) => {
|
||||
export const get = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
where: {
|
||||
@ -51,14 +51,14 @@ export const get = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const list = (server, a, b, names) => {
|
||||
export const list = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -77,16 +77,16 @@ export const list = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scope = (server, a, b, names) => {
|
||||
export const scope = async (server, a, b, names) => {
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -116,7 +116,7 @@ export const scope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scopeScope = (server, a, b, names) => {
|
||||
export const scopeScope = async (server, a, b, names) => {
|
||||
const scopes = {
|
||||
a: Object.keys(a.options.scopes),
|
||||
b: Object.keys(b.options.scopes),
|
||||
@ -124,11 +124,11 @@ export const scopeScope = (server, a, b, names) => {
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
||||
path: `${prefix}${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const list = await b.scope(request.params.scopeb).findAll({
|
||||
@ -152,14 +152,14 @@ export const scopeScope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (server, a, b, names) => {
|
||||
export const destroy = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -179,16 +179,16 @@ export const destroy = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyScope = (server, a, b, names) => {
|
||||
export const destroyScope = async (server, a, b, names) => {
|
||||
const scopes = Object.keys(b.options.scopes);
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -221,14 +221,14 @@ export const destroyScope = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const update = (server, a, b, names) => {
|
||||
export const update = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
|
@ -14,14 +14,14 @@ export default (server, a, b, names, options) => {
|
||||
update(server, a, b, names);
|
||||
};
|
||||
|
||||
export const get = (server, a, b, names) => {
|
||||
export const get = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -47,7 +47,7 @@ export const get = (server, a, b, names) => {
|
||||
export const create = (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`,
|
||||
path: `${prefix}${names.a.singular}/{id}/${names.b.singular}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
@ -67,14 +67,14 @@ export const create = (server, a, b, names) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (server, a, b, names) => {
|
||||
export const destroy = async (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
@ -99,11 +99,11 @@ export const destroy = (server, a, b, names) => {
|
||||
export const update = (server, a, b, names) => {
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
const base = await a.findOne({
|
||||
|
@ -6,7 +6,7 @@ const STATUS_OK = 200;
|
||||
|
||||
setup(test);
|
||||
|
||||
test('belongsTo /team?include=city', async (t) => {
|
||||
test('belongsTo /team?include=city', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, city1 } = instances;
|
||||
const path = `/team/${team1.id}?include=city`;
|
||||
@ -17,7 +17,7 @@ test('belongsTo /team?include=city', async (t) => {
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('belongsTo /team?include=cities', async (t) => {
|
||||
test('belongsTo /team?include=cities', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, city1 } = instances;
|
||||
const path = `/team/${team1.id}?include=cities`;
|
||||
@ -28,7 +28,7 @@ test('belongsTo /team?include=cities', async (t) => {
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('hasMany /team?include=player', async (t) => {
|
||||
test('hasMany /team?include=player', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, player1, player2 } = instances;
|
||||
const path = `/team/${team1.id}?include=player`;
|
||||
@ -42,7 +42,7 @@ test('hasMany /team?include=player', async (t) => {
|
||||
t.truthy(playerIds.includes(player2.id));
|
||||
});
|
||||
|
||||
test('hasMany /team?include=players', async (t) => {
|
||||
test('hasMany /team?include=players', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, player1, player2 } = instances;
|
||||
const path = `/team/${team1.id}?include=players`;
|
||||
@ -56,7 +56,18 @@ test('hasMany /team?include=players', async (t) => {
|
||||
t.truthy(playerIds.includes(player2.id));
|
||||
});
|
||||
|
||||
test('multiple includes /team?include=players&include=city', async (t) => {
|
||||
test('belongsTo with alias /player?include={"model": "Master", "as": "Coach"}', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, master1 } = instances;
|
||||
const path = `/player/${team1.id}?include={"model": "Master", "as": "Coach"}`;
|
||||
|
||||
const { result, statusCode } = await server.inject(path);
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, team1.id);
|
||||
t.is(result.Coach.id, master1.id);
|
||||
});
|
||||
|
||||
test('multiple includes /team?include=players&include=city', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, player1, player2, city1 } = instances;
|
||||
const path = `/team/${team1.id}?include=players&include=city`;
|
||||
@ -71,7 +82,7 @@ test('multiple includes /team?include=players&include=city', async (t) => {
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('multiple includes /team?include[]=players&include[]=city', async (t) => {
|
||||
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`;
|
||||
@ -85,3 +96,67 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => {
|
||||
t.truthy(playerIds.includes(player2.id));
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('multiple includes /team?include[]=players&include[]={"model": "City"}', async(t) => {
|
||||
const { server, instances } = t.context;
|
||||
const { team1, player1, player2, city1 } = instances;
|
||||
const path = `/team/${team1.id}?include[]=players&include[]={"model": "City"}`;
|
||||
|
||||
const { result, statusCode } = await server.inject(path);
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, team1.id);
|
||||
|
||||
const playerIds = result.Players.map(({ id }) => id);
|
||||
t.truthy(playerIds.includes(player1.id));
|
||||
t.truthy(playerIds.includes(player2.id));
|
||||
t.is(result.City.id, city1.id);
|
||||
});
|
||||
|
||||
test('include filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||
, async(t) => {
|
||||
const { server } = t.context;
|
||||
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
});
|
||||
|
||||
test('nested include filter ' +
|
||||
'/citiy?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, team1, team2 } = instances;
|
||||
const url = '/city?include[]=' +
|
||||
'{"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}';
|
||||
const method = 'GET';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result.id, city1.id);
|
||||
|
||||
const teamIds = result.Teams.map(({ id }) => id);
|
||||
t.truthy(teamIds.includes(team1.id));
|
||||
t.truthy(teamIds.includes(team2.id));
|
||||
});
|
||||
|
||||
test('complex include ' +
|
||||
'/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}'
|
||||
, async(t) => {
|
||||
const { instances, server } = t.context;
|
||||
const { city1, master1, player2, team1 } = instances;
|
||||
const method = 'GET';
|
||||
const url = '/cities?include[]={"model":"Team", ' +
|
||||
'"include":{ "model":"Player", "where":{"name": "Pinot"}, ' +
|
||||
'"include":{ "model":"Master", "as":"Coach", "where":{"name": "Shifu"}}}}';
|
||||
|
||||
const { result, statusCode } = await server.inject({ url, method });
|
||||
t.is(statusCode, STATUS_OK);
|
||||
t.is(result[0].id, city1.id);
|
||||
t.is(result[0].Teams[0].id, team1.id);
|
||||
t.is(result[0].Teams[0].Players[0].id, player2.id);
|
||||
t.is(result[0].Teams[0].Players[0].Coach.id, master1.id);
|
||||
});
|
||||
|
102
src/crud.js
102
src/crud.js
@ -5,16 +5,16 @@ import _ from 'lodash';
|
||||
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
|
||||
import { notFound } from 'boom';
|
||||
import * as associations from './associations/index';
|
||||
import getConfigForMethod from './get-config-for-method.js';
|
||||
import getConfigForMethod, { sequelizeOperators } from './get-config-for-method.js';
|
||||
|
||||
const createAll = ({
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
scopes,
|
||||
server,
|
||||
model,
|
||||
prefix,
|
||||
config,
|
||||
attributeValidation,
|
||||
associationValidation,
|
||||
scopes,
|
||||
}) => {
|
||||
Object.keys(methods).forEach((method) => {
|
||||
methods[method]({
|
||||
@ -35,27 +35,28 @@ const createAll = ({
|
||||
export { associations };
|
||||
|
||||
/*
|
||||
The `models` option, becomes `permissions`, and can look like:
|
||||
The `models` option, becomes `permissions`, and can look like:
|
||||
|
||||
```
|
||||
models: ['cat', 'dog']
|
||||
```
|
||||
```
|
||||
models: ['cat', 'dog']
|
||||
```
|
||||
|
||||
or
|
||||
or
|
||||
|
||||
```
|
||||
models: {
|
||||
cat: ['list', 'get']
|
||||
, dog: true // all
|
||||
}
|
||||
```
|
||||
```
|
||||
models: {
|
||||
cat: ['list', 'get']
|
||||
, dog: true // all
|
||||
}
|
||||
```
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
export default (server, model, { prefix, defaultConfig: config, models: permissions }) => {
|
||||
const modelName = model._singular;
|
||||
const modelAttributes = Object.keys(model.attributes);
|
||||
const associatedModelNames = Object.keys(model.associations);
|
||||
const associatedModelAliases = _.map(model.associations, (assoc => assoc.as));
|
||||
const modelAssociations = [
|
||||
...associatedModelNames,
|
||||
..._.flatMap(associatedModelNames, (associationName) => {
|
||||
@ -71,13 +72,28 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
return params;
|
||||
}, {});
|
||||
|
||||
const validAssociations = modelAssociations.length
|
||||
? joi.string().valid(...modelAssociations)
|
||||
: joi.valid(null);
|
||||
const modelsHasAssociations = modelAssociations && modelAssociations.length;
|
||||
const validAssociationsString = modelsHasAssociations
|
||||
? joi.string().valid(...modelAssociations)
|
||||
: joi.valid(null);
|
||||
const validAssociationsObject = modelsHasAssociations
|
||||
? joi.object().keys({
|
||||
model: joi.string().valid(...modelAssociations),
|
||||
where: joi.object().keys({
|
||||
...attributeValidation,
|
||||
...sequelizeOperators,
|
||||
}),
|
||||
as: joi.string().valid(...associatedModelAliases),
|
||||
include: joi.any(), // @Todo: should validate the same as associationValidation var below
|
||||
})
|
||||
: joi.valid(null);
|
||||
const associationValidation = {
|
||||
include: [joi.array().items(validAssociations), validAssociations],
|
||||
include: [
|
||||
joi.array().items(validAssociationsString, validAssociationsObject),
|
||||
validAssociationsString,
|
||||
validAssociationsObject,
|
||||
],
|
||||
};
|
||||
|
||||
const scopes = Object.keys(model.options.scopes);
|
||||
|
||||
// if we don't have any permissions set, just create all the methods
|
||||
@ -91,11 +107,11 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
associationValidation,
|
||||
scopes,
|
||||
});
|
||||
// if permissions are set, but we can't parse them, throw an error
|
||||
// if permissions are set, but we can't parse them, throw an error
|
||||
} else if (!Array.isArray(permissions)) {
|
||||
throw new Error('hapi-sequelize-crud: `models` property must be an array');
|
||||
// if permissions are set, but the only thing we've got is a model name, there
|
||||
// are no permissions to be set, so just create all methods and move on
|
||||
// if permissions are set, but the only thing we've got is a model name, there
|
||||
// are no permissions to be set, so just create all methods and move on
|
||||
} else if (permissions.includes(modelName)) {
|
||||
createAll({
|
||||
server,
|
||||
@ -106,7 +122,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
associationValidation,
|
||||
scopes,
|
||||
});
|
||||
// if we've gotten here, we have complex permissions and need to set them
|
||||
// if we've gotten here, we have complex permissions and need to set them
|
||||
} else {
|
||||
const permissionOptions = permissions.filter((permission) => {
|
||||
return permission.model === modelName;
|
||||
@ -147,14 +163,14 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
||||
}
|
||||
};
|
||||
|
||||
export const list = ({ server, model, prefix = '/', config }) => {
|
||||
export const list = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._plural),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { limit, offset } = parseLimitAndOffset(request);
|
||||
const order = parseOrder(request);
|
||||
@ -174,14 +190,14 @@ export const list = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const get = ({ server, model, prefix = '/', config }) => {
|
||||
export const get = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._singular, '{id?}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { id } = request.params;
|
||||
if (id) where[model.primaryKeyField] = id;
|
||||
@ -198,14 +214,14 @@ export const get = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const scope = ({ server, model, prefix = '/', config }) => {
|
||||
export const scope = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: path.join(prefix, model._plural, '{scope}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
const { limit, offset } = parseLimitAndOffset(request);
|
||||
const order = parseOrder(request);
|
||||
@ -255,9 +271,9 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
|
||||
|
||||
if (!list.length) {
|
||||
return void reply(id
|
||||
? notFound(`${id} not found.`)
|
||||
: notFound('Nothing found.')
|
||||
);
|
||||
? notFound(`${id} not found.`)
|
||||
: notFound('Nothing found.')
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(list.map(instance => instance.destroy()));
|
||||
@ -284,9 +300,9 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
||||
|
||||
if (!list.length) {
|
||||
return void reply(id
|
||||
? notFound(`${id} not found.`)
|
||||
: notFound('Nothing found.')
|
||||
);
|
||||
? notFound(`${id} not found.`)
|
||||
: notFound('Nothing found.')
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(list.map(instance => instance.destroy()));
|
||||
@ -299,14 +315,14 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const destroyScope = ({ server, model, prefix = '/', config }) => {
|
||||
export const destroyScope = async ({ server, model, prefix = '/', config }) => {
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: path.join(prefix, model._plural, '{scope}'),
|
||||
|
||||
@error
|
||||
async handler(request, reply) {
|
||||
const include = parseInclude(request);
|
||||
const include = await parseInclude(request);
|
||||
const where = parseWhere(request);
|
||||
|
||||
if (include instanceof Error) return void reply(include);
|
||||
|
@ -38,6 +38,7 @@ const register = (server, options = {}, next) => {
|
||||
// Join tables
|
||||
if (model.options.name.singular !== model.name) continue;
|
||||
|
||||
|
||||
for (const key of Object.keys(model.associations)) {
|
||||
const association = model.associations[key];
|
||||
const { source, target } = association;
|
||||
@ -94,15 +95,11 @@ const register = (server, options = {}, next) => {
|
||||
|
||||
// build the methods for each model now that we've defined all the
|
||||
// associations
|
||||
Object.keys(models).filter((modelName) => {
|
||||
const model = models[modelName];
|
||||
return model.options.name.singular === model.name;
|
||||
}).forEach((modelName) => {
|
||||
Object.keys(models).forEach((modelName) => {
|
||||
const model = models[modelName];
|
||||
crud(server, model, options);
|
||||
});
|
||||
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
73
src/utils.js
73
src/utils.js
@ -1,5 +1,7 @@
|
||||
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
|
||||
import { notImplemented } from 'boom';
|
||||
import joi from 'joi';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
|
||||
|
||||
@ -8,7 +10,7 @@ const getModels = (request) => {
|
||||
const noRequestModels = !request.models;
|
||||
if (noGetDb && noRequestModels) {
|
||||
return notImplemented('`request.getDb` or `request.models` are not defined.'
|
||||
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
|
||||
+ 'Be sure to load hapi-sequelize before hapi-sequelize-crud.');
|
||||
}
|
||||
|
||||
const { models } = noGetDb ? request : request.getDb();
|
||||
@ -16,31 +18,68 @@ const getModels = (request) => {
|
||||
return models;
|
||||
};
|
||||
|
||||
export const parseInclude = request => {
|
||||
const getModelInstance = (models, includeItem) => {
|
||||
return new Promise(async(resolve) => {
|
||||
if (includeItem) {
|
||||
if (typeof includeItem !== 'object') {
|
||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
||||
const { _singular, _plural } = models[modelName];
|
||||
return _singular === includeItem || _plural === includeItem;
|
||||
});
|
||||
|
||||
if (singluarOrPluralMatch) {
|
||||
return resolve(models[singluarOrPluralMatch]);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof includeItem === 'string' && models.hasOwnProperty(includeItem)) {
|
||||
return resolve(models[includeItem]);
|
||||
} else if (typeof includeItem === 'object') {
|
||||
if (
|
||||
typeof includeItem.model === 'string' &&
|
||||
includeItem.model.length &&
|
||||
models.hasOwnProperty(includeItem.model)
|
||||
) {
|
||||
includeItem.model = models[includeItem.model];
|
||||
}
|
||||
if (includeItem.hasOwnProperty('include')) {
|
||||
includeItem.include = await getModelInstance(models, includeItem.include);
|
||||
return resolve(includeItem);
|
||||
} else {
|
||||
return resolve(includeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolve(includeItem);
|
||||
});
|
||||
};
|
||||
|
||||
export const parseInclude = async(request) => {
|
||||
if (typeof request.query.include === 'undefined') return [];
|
||||
|
||||
const include = Array.isArray(request.query.include)
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
? request.query.include
|
||||
: [request.query.include]
|
||||
;
|
||||
|
||||
const models = getModels(request);
|
||||
if (models.isBoom) return models;
|
||||
|
||||
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) {
|
||||
a.model = models[a.model];
|
||||
const jsonValidation = joi.string().regex(/^\{.*?"model":.*?\}$/);
|
||||
const includes = include.map(async(b) => {
|
||||
let a = b;
|
||||
try {
|
||||
if (!jsonValidation.validate(a).error) {
|
||||
a = JSON.parse(b);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
return a;
|
||||
return getModelInstance(models, a);
|
||||
}).filter(identity);
|
||||
|
||||
return await Promise.all(includes);
|
||||
};
|
||||
|
||||
export const parseWhere = request => {
|
||||
|
18
test/fixtures/models/master.js
vendored
Normal file
18
test/fixtures/models/master.js
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
export default (sequelize, DataTypes) => {
|
||||
return sequelize.define('Master', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: (models) => {
|
||||
models.Master.hasMany(models.Player, {
|
||||
foreignKey: 'coachId'
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
4
test/fixtures/models/player.js
vendored
4
test/fixtures/models/player.js
vendored
@ -14,6 +14,10 @@ export default (sequelize, DataTypes) => {
|
||||
models.Player.belongsTo(models.Team, {
|
||||
foreignKey: { name: 'teamId' },
|
||||
});
|
||||
models.Player.belongsTo(models.Master, {
|
||||
foreignKey: 'coachId',
|
||||
as: 'Coach',
|
||||
});
|
||||
},
|
||||
},
|
||||
scopes: {
|
||||
|
@ -14,15 +14,16 @@ const modelNames = [
|
||||
{ Singluar: 'City', singular: 'city', Plural: 'Cities', plural: 'cities' },
|
||||
{ Singluar: 'Team', singular: 'team', Plural: 'Teams', plural: 'teams' },
|
||||
{ Singluar: 'Player', singular: 'player', Plural: 'Players', plural: 'players' },
|
||||
{ Singluar: 'Master', singular: 'master', Plural: 'Masters', plural: 'masters' },
|
||||
];
|
||||
|
||||
|
||||
export default (test) => {
|
||||
test.beforeEach('get an open port', async (t) => {
|
||||
test.beforeEach('get an open port', async(t) => {
|
||||
t.context.port = await getPort();
|
||||
});
|
||||
|
||||
test.beforeEach('setup server', async (t) => {
|
||||
test.beforeEach('setup server', async(t) => {
|
||||
const sequelize = t.context.sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
logging: false,
|
||||
@ -46,25 +47,33 @@ export default (test) => {
|
||||
});
|
||||
|
||||
await server.register({
|
||||
register: require('../src/index.js'),
|
||||
options: {
|
||||
name: dbName,
|
||||
register: require('../src/index.js'),
|
||||
options: {
|
||||
name: dbName,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.beforeEach('create data', async (t) => {
|
||||
const { Player, Team, City } = t.context.sequelize.models;
|
||||
test.beforeEach('create data', async(t) => {
|
||||
const { Player, Master, Team, City } = t.context.sequelize.models;
|
||||
const city1 = await City.create({ name: 'Healdsburg' });
|
||||
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
|
||||
const team2 = await Team.create({ name: 'Footballs', cityId: city1.id });
|
||||
const master1 = await Master.create({ name: 'Shifu' });
|
||||
const master2 = await Master.create({ name: 'Oogway' });
|
||||
const player1 = await Player.create({
|
||||
name: 'Cat', teamId: team1.id, active: true,
|
||||
name: 'Cat', teamId: team1.id, active: true, coachId: master1.id
|
||||
});
|
||||
const player2 = await Player.create({ name: 'Pinot', teamId: team1.id });
|
||||
const player3 = await Player.create({ name: 'Syrah', teamId: team2.id });
|
||||
t.context.instances = { city1, team1, team2, player1, player2, player3 };
|
||||
const player2 = await Player.create({
|
||||
name: 'Pinot', teamId: team1.id, coachId: master1.id
|
||||
});
|
||||
const player3 = await Player.create({
|
||||
name: 'Syrah', teamId: team2.id, coachId: master2.id
|
||||
});
|
||||
t.context.instances = {
|
||||
city1, team1, team2, player1, player2, player3, master1, master2
|
||||
};
|
||||
});
|
||||
|
||||
// kill the server so that we can exit and don't leak memory
|
||||
|
Loading…
Reference in New Issue
Block a user