Add feature to allow nested include and filtering relationships/associations #36
16
README.md
16
README.md
@ -92,13 +92,25 @@ It's easy to restrict your requests using Sequelize's `where` query option. Just
|
|||||||
Team.findOne({ where: { city: 'windsor' }})
|
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
|
## `include` queries
|
||||||
Getting related models is easy, just use a query parameter `include`.
|
Getting related models is easy, just use a query parameter `include`.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// returns all teams with their related City model
|
// returns all teams with their related City model
|
||||||
// GET /teams?include=city or
|
// GET /teams?include=city or
|
||||||
// GET /teams?include={"include": {"model": "City"}}}
|
// GET /teams?include={"model": "City"}
|
||||||
|
|
||||||
|
|
||||||
// results in a Sequelize query:
|
// results in a Sequelize query:
|
||||||
@ -126,7 +138,7 @@ Team.findAll({include: [Player]})
|
|||||||
Filtering by related models property, you can pass **where** paremeter inside each **include** item(s) object.
|
Filtering by related models property, you can pass **where** paremeter inside each **include** item(s) object.
|
||||||
```js
|
```js
|
||||||
// returns all team with their related City where City property name equals Healdsburg
|
// returns all team with their related City where City property name equals Healdsburg
|
||||||
// GET /teams?include={"include": {"model": "City", "where": {"name": "Healdsburg"}}}
|
// GET /teams?include={"model": "City", "where": {"name": "Healdsburg"}}
|
||||||
|
|
||||||
// results in a Sequelize query:
|
// results in a Sequelize query:
|
||||||
Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}})
|
Team.findAll({include: {model: City, where: {name: 'Healdsburg'}}})
|
||||||
|
55
src/crud.js
55
src/crud.js
@ -5,16 +5,16 @@ import _ from 'lodash';
|
|||||||
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
|
import { parseInclude, parseWhere, parseLimitAndOffset, parseOrder } from './utils';
|
||||||
import { notFound } from 'boom';
|
import { notFound } from 'boom';
|
||||||
import * as associations from './associations/index';
|
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 = ({
|
const createAll = ({
|
||||||
server,
|
server,
|
||||||
model,
|
model,
|
||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
scopes,
|
scopes,
|
||||||
}) => {
|
}) => {
|
||||||
Object.keys(methods).forEach((method) => {
|
Object.keys(methods).forEach((method) => {
|
||||||
methods[method]({
|
methods[method]({
|
||||||
@ -24,7 +24,7 @@ const createAll = ({
|
|||||||
config: getConfigForMethod({
|
config: getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
config,
|
config,
|
||||||
scopes,
|
scopes,
|
||||||
}),
|
}),
|
||||||
@ -71,6 +71,27 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
return params;
|
return params;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const modelsHasAssociations = modelAssociations && modelAssociations.length;
|
||||||
|
const validAssociationsString = modelsHasAssociations
|
||||||
|
? joi.string().valid(...modelAssociations)
|
||||||
|
: joi.valid(null);
|
||||||
|
const validAssociationsObject = modelsHasAssociations
|
||||||
|
? joi.object().keys({
|
||||||
|
model: joi.string().valid(...modelAssociations),
|
||||||
|
where: joi.object().keys({
|
||||||
|
...attributeValidation,
|
||||||
|
...sequelizeOperators,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: joi.valid(null);
|
||||||
|
const associationValidation = {
|
||||||
|
include: [
|
||||||
|
joi.array().items(validAssociationsString),
|
||||||
|
joi.array().items(validAssociationsObject),
|
||||||
|
validAssociationsString,
|
||||||
|
validAssociationsObject,
|
||||||
|
],
|
||||||
|
};
|
||||||
const scopes = Object.keys(model.options.scopes);
|
const scopes = Object.keys(model.options.scopes);
|
||||||
|
|
||||||
// if we don't have any permissions set, just create all the methods
|
// if we don't have any permissions set, just create all the methods
|
||||||
@ -81,7 +102,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
scopes,
|
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
|
||||||
@ -96,7 +117,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
prefix,
|
prefix,
|
||||||
config,
|
config,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
scopes,
|
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
|
||||||
@ -118,7 +139,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
config: getConfigForMethod({
|
config: getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
|
|||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
scopes,
|
scopes,
|
||||||
config: permissionConfig,
|
config: permissionConfig,
|
||||||
}),
|
}),
|
||||||
@ -130,7 +151,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
model,
|
model,
|
||||||
prefix,
|
prefix,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
scopes,
|
scopes,
|
||||||
config: permissionConfig,
|
config: permissionConfig,
|
||||||
@joeybaker and @mdibaiee, i need help to apply include child joi validation to be the same as its parent include validation (nested validations with the same schema) @joeybaker and @mdibaiee, i need help to apply include child joi validation to be the same as its parent include validation (nested validations with the same schema)
|
|||||||
});
|
});
|
||||||
@ -248,8 +269,8 @@ export const destroy = ({ server, model, prefix = '/', config }) => {
|
|||||||
|
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
return void reply(id
|
return void reply(id
|
||||||
? notFound(`${id} not found.`)
|
? notFound(`${id} not found.`)
|
||||||
: notFound('Nothing found.')
|
: notFound('Nothing found.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,8 +298,8 @@ export const destroyAll = ({ server, model, prefix = '/', config }) => {
|
|||||||
|
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
return void reply(id
|
return void reply(id
|
||||||
? notFound(`${id} not found.`)
|
? notFound(`${id} not found.`)
|
||||||
: notFound('Nothing found.')
|
: notFound('Nothing found.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ export const restrictMethods = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default ({
|
export default ({
|
||||||
method, attributeValidation, modelAssociations, scopes = [], config = {},
|
method, attributeValidation, associationValidation, scopes = [], config = {},
|
||||||
}) => {
|
}) => {
|
||||||
const hasWhere = whereMethods.includes(method);
|
const hasWhere = whereMethods.includes(method);
|
||||||
const hasInclude = includeMethods.includes(method);
|
const hasInclude = includeMethods.includes(method);
|
||||||
@ -96,27 +96,9 @@ export default ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasInclude) {
|
if (hasInclude) {
|
||||||
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,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
: joi.valid(null);
|
|
||||||
const query = concatToJoiObject(joi.object()
|
const query = concatToJoiObject(joi.object()
|
||||||
.keys({
|
.keys({
|
||||||
include: [
|
...associationValidation,
|
||||||
joi.array().items(validAssociationsString),
|
|
||||||
joi.array().items(validAssociationsObject),
|
|
||||||
validAssociationsString,
|
|
||||||
validAssociationsObject,
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
get(methodConfig, 'validate.query')
|
get(methodConfig, 'validate.query')
|
||||||
);
|
);
|
||||||
|
@ -20,19 +20,8 @@ test.beforeEach((t) => {
|
|||||||
myKey: joi.any(),
|
myKey: joi.any(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const validAssociationsString = joi.string().valid(...t.context.models);
|
|
||||||
const validAssociationsObject = joi.object().keys({
|
|
||||||
model: joi.string().valid(...t.context.models),
|
|
||||||
where: joi.object(),
|
|
||||||
});
|
|
||||||
|
|
||||||
t.context.associationValidation = {
|
t.context.associationValidation = {
|
||||||
include: [
|
include: joi.array().items(joi.string().valid(t.context.models)),
|
||||||
joi.array().items(validAssociationsString),
|
|
||||||
joi.array().items(validAssociationsObject),
|
|
||||||
validAssociationsString,
|
|
||||||
validAssociationsObject,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
t.context.config = {
|
t.context.config = {
|
||||||
@ -162,12 +151,12 @@ test('query attributeValidation w/ config as joi object', (t) => {
|
|||||||
|
|
||||||
test('validate.query associationValidation', (t) => {
|
test('validate.query associationValidation', (t) => {
|
||||||
const { attributeValidation, associationValidation, models } = t.context;
|
const { attributeValidation, associationValidation, models } = t.context;
|
||||||
const modelAssociations = models;
|
|
||||||
includeMethods.forEach((method) => {
|
includeMethods.forEach((method) => {
|
||||||
const configForMethod = getConfigForMethod({
|
const configForMethod = getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
attributeValidation,
|
attributeValidation,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
});
|
});
|
||||||
const { query } = configForMethod.validate;
|
const { query } = configForMethod.validate;
|
||||||
|
|
||||||
@ -194,7 +183,6 @@ test('validate.query associationValidation', (t) => {
|
|||||||
|
|
||||||
test('query associationValidation w/ config as plain object', (t) => {
|
test('query associationValidation w/ config as plain object', (t) => {
|
||||||
const { associationValidation, models } = t.context;
|
const { associationValidation, models } = t.context;
|
||||||
const modelAssociations = models;
|
|
||||||
const config = {
|
const config = {
|
||||||
validate: {
|
validate: {
|
||||||
query: {
|
query: {
|
||||||
@ -206,7 +194,7 @@ test('query associationValidation w/ config as plain object', (t) => {
|
|||||||
includeMethods.forEach((method) => {
|
includeMethods.forEach((method) => {
|
||||||
const configForMethod = getConfigForMethod({
|
const configForMethod = getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
const { query } = configForMethod.validate;
|
const { query } = configForMethod.validate;
|
||||||
@ -234,7 +222,6 @@ test('query associationValidation w/ config as plain object', (t) => {
|
|||||||
|
|
||||||
test('query associationValidation w/ config as joi object', (t) => {
|
test('query associationValidation w/ config as joi object', (t) => {
|
||||||
const { associationValidation, models } = t.context;
|
const { associationValidation, models } = t.context;
|
||||||
const modelAssociations = models;
|
|
||||||
const queryKeys = {
|
const queryKeys = {
|
||||||
aKey: joi.boolean(),
|
aKey: joi.boolean(),
|
||||||
};
|
};
|
||||||
@ -247,7 +234,7 @@ test('query associationValidation w/ config as joi object', (t) => {
|
|||||||
includeMethods.forEach((method) => {
|
includeMethods.forEach((method) => {
|
||||||
const configForMethod = getConfigForMethod({
|
const configForMethod = getConfigForMethod({
|
||||||
method,
|
method,
|
||||||
modelAssociations,
|
associationValidation,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
const { query } = configForMethod.validate;
|
const { query } = configForMethod.validate;
|
||||||
|
Loading…
Reference in New Issue
Block a user
Why move this validation into
get-config-for-method
? All other validation is defined in this method already.Sorry @joeybaker i have fixed this, moved to crud.js