Add feature to allow nested include and filtering relationships/associations #36
@ -19,14 +19,14 @@ export default (server, a, b, names, options) => {
|
|||||||
update(server, a, b, names);
|
update(server, a, b, names);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const get = (server, a, b, names) => {
|
export const get = async (server, a, b, names) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
const base = await a.findOne({
|
||||||
where: {
|
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({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
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);
|
const scopes = Object.keys(b.options.scopes);
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
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 = {
|
const scopes = {
|
||||||
a: Object.keys(a.options.scopes),
|
a: Object.keys(a.options.scopes),
|
||||||
b: Object.keys(b.options.scopes),
|
b: Object.keys(b.options.scopes),
|
||||||
@ -124,11 +124,11 @@ export const scopeScope = (server, a, b, names) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
path: `${prefix}${names.a.plural}/{scopea}/${names.b.plural}/{scopeb}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const list = await b.scope(request.params.scopeb).findAll({
|
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({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
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);
|
const scopes = Object.keys(b.options.scopes);
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}/{scope}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
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({
|
server.route({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.plural}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.plural}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
const base = await a.findOne({
|
||||||
|
@ -14,14 +14,14 @@ export default (server, a, b, names, options) => {
|
|||||||
update(server, a, b, names);
|
update(server, a, b, names);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const get = (server, a, b, names) => {
|
export const get = async (server, a, b, names) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
const base = await a.findOne({
|
||||||
@ -47,7 +47,7 @@ export const get = (server, a, b, names) => {
|
|||||||
export const create = (server, a, b, names) => {
|
export const create = (server, a, b, names) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `${prefix}/${names.a.singular}/{id}/${names.b.singular}`,
|
path: `${prefix}${names.a.singular}/{id}/${names.b.singular}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
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({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
const base = await a.findOne({
|
||||||
@ -99,11 +99,11 @@ export const destroy = (server, a, b, names) => {
|
|||||||
export const update = (server, a, b, names) => {
|
export const update = (server, a, b, names) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
path: `${prefix}/${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
path: `${prefix}${names.a.singular}/{aid}/${names.b.singular}/{bid}`,
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
const base = await a.findOne({
|
const base = await a.findOne({
|
||||||
|
@ -6,7 +6,7 @@ const STATUS_OK = 200;
|
|||||||
|
|
||||||
setup(test);
|
setup(test);
|
||||||
|
|
||||||
test('belongsTo /team?include=city', async (t) => {
|
test('belongsTo /team?include=city', async(t) => {
|
||||||
const { server, instances } = t.context;
|
const { server, instances } = t.context;
|
||||||
|
|||||||
const { team1, city1 } = instances;
|
const { team1, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=city`;
|
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);
|
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 { server, instances } = t.context;
|
||||||
const { team1, city1 } = instances;
|
const { team1, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=cities`;
|
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);
|
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 { server, instances } = t.context;
|
||||||
const { team1, player1, player2 } = instances;
|
const { team1, player1, player2 } = instances;
|
||||||
const path = `/team/${team1.id}?include=player`;
|
const path = `/team/${team1.id}?include=player`;
|
||||||
@ -42,7 +42,7 @@ test('hasMany /team?include=player', async (t) => {
|
|||||||
t.truthy(playerIds.includes(player2.id));
|
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 { server, instances } = t.context;
|
||||||
const { team1, player1, player2 } = instances;
|
const { team1, player1, player2 } = instances;
|
||||||
const path = `/team/${team1.id}?include=players`;
|
const path = `/team/${team1.id}?include=players`;
|
||||||
@ -56,7 +56,18 @@ test('hasMany /team?include=players', async (t) => {
|
|||||||
t.truthy(playerIds.includes(player2.id));
|
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 { server, instances } = t.context;
|
||||||
const { team1, player1, player2, city1 } = instances;
|
const { team1, player1, player2, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include=players&include=city`;
|
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);
|
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 { server, instances } = t.context;
|
||||||
const { team1, player1, player2, city1 } = instances;
|
const { team1, player1, player2, city1 } = instances;
|
||||||
const path = `/team/${team1.id}?include[]=players&include[]=city`;
|
const path = `/team/${team1.id}?include[]=players&include[]=city`;
|
||||||
@ -86,8 +97,23 @@ test('multiple includes /team?include[]=players&include[]=city', async (t) => {
|
|||||||
t.is(result.City.id, city1.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('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}'
|
||||||
, async (t) => {
|
, async(t) => {
|
||||||
const { server } = t.context;
|
const { server } = t.context;
|
||||||
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
const url = '/teams?include[]={"model": "City", "where": {"name": "Healdsburg"}}';
|
||||||
const method = 'GET';
|
const method = 'GET';
|
||||||
@ -95,3 +121,37 @@ test('inlcude filter /teams?include[]={"model": "City", "where": {"name": "Heald
|
|||||||
const { statusCode } = await server.inject({ url, method });
|
const { statusCode } = await server.inject({ url, method });
|
||||||
t.is(statusCode, STATUS_OK);
|
t.is(statusCode, STATUS_OK);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('nested inlcude filter ' +
|
||||||
|
'/city?include[]={"model": "Team", "include": {"model": "Player", "where": {"name": "Pinot"}}}'
|
||||||
|
, async(t) => {
|
||||||
|
const { instances, server } = t.context;
|
||||||
|
const { city1, team1, player2 } = instances;
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const url = '/city?include[]={"model": "Team", "include": {"model": "Player", "where": {"name": "Pinot"}}}';
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
const { result, statusCode } = await server.inject({ url, method });
|
||||||
|
t.is(statusCode, STATUS_OK);
|
||||||
|
t.is(result.id, city1.id);
|
||||||
|
t.is(result.Teams[0].id, team1.id);
|
||||||
|
t.is(result.Teams[0].Players[0].id, player2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nested inlcude filter ' +
|
||||||
|
'/city?include[]={"model": "Team", "include": {"model": "City", "where": {"name": "Healdsburg"}}}'
|
||||||
|
, async(t) => {
|
||||||
|
const { instances, server } = t.context;
|
||||||
|
const { city1, team1, team2 } = instances;
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
22
src/crud.js
@ -56,6 +56,7 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
const modelName = model._singular;
|
const modelName = model._singular;
|
||||||
const modelAttributes = Object.keys(model.attributes);
|
const modelAttributes = Object.keys(model.attributes);
|
||||||
const associatedModelNames = Object.keys(model.associations);
|
const associatedModelNames = Object.keys(model.associations);
|
||||||
|
const associatedModelAliases = _.map(model.associations, (assoc => assoc.as));
|
||||||
const modelAssociations = [
|
const modelAssociations = [
|
||||||
...associatedModelNames,
|
...associatedModelNames,
|
||||||
..._.flatMap(associatedModelNames, (associationName) => {
|
..._.flatMap(associatedModelNames, (associationName) => {
|
||||||
@ -82,12 +83,13 @@ export default (server, model, { prefix, defaultConfig: config, models: permissi
|
|||||||
...attributeValidation,
|
...attributeValidation,
|
||||||
...sequelizeOperators,
|
...sequelizeOperators,
|
||||||
}),
|
}),
|
||||||
|
as: joi.string().valid(...associatedModelAliases),
|
||||||
|
include: joi.any(), // @Todo: should validate the same as associationValidation var below
|
||||||
})
|
})
|
||||||
: joi.valid(null);
|
: joi.valid(null);
|
||||||
const associationValidation = {
|
const associationValidation = {
|
||||||
include: [
|
include: [
|
||||||
joi.array().items(validAssociationsString),
|
joi.array().items(validAssociationsString, validAssociationsObject),
|
||||||
joi.array().items(validAssociationsObject),
|
|
||||||
validAssociationsString,
|
validAssociationsString,
|
||||||
validAssociationsObject,
|
validAssociationsObject,
|
||||||
],
|
],
|
||||||
@ -161,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({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: path.join(prefix, model._plural),
|
path: path.join(prefix, model._plural),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
const { limit, offset } = parseLimitAndOffset(request);
|
const { limit, offset } = parseLimitAndOffset(request);
|
||||||
const order = parseOrder(request);
|
const order = parseOrder(request);
|
||||||
@ -188,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({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: path.join(prefix, model._singular, '{id?}'),
|
path: path.join(prefix, model._singular, '{id?}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
if (id) where[model.primaryKeyField] = id;
|
if (id) where[model.primaryKeyField] = id;
|
||||||
@ -212,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({
|
server.route({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: path.join(prefix, model._plural, '{scope}'),
|
path: path.join(prefix, model._plural, '{scope}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
const { limit, offset } = parseLimitAndOffset(request);
|
const { limit, offset } = parseLimitAndOffset(request);
|
||||||
const order = parseOrder(request);
|
const order = parseOrder(request);
|
||||||
@ -313,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({
|
server.route({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
path: path.join(prefix, model._plural, '{scope}'),
|
path: path.join(prefix, model._plural, '{scope}'),
|
||||||
|
|
||||||
@error
|
@error
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
const include = parseInclude(request);
|
const include = await parseInclude(request);
|
||||||
const where = parseWhere(request);
|
const where = parseWhere(request);
|
||||||
|
|
||||||
if (include instanceof Error) return void reply(include);
|
if (include instanceof Error) return void reply(include);
|
||||||
|
71
src/utils.js
@ -1,6 +1,7 @@
|
|||||||
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
|
import { omit, identity, toNumber, isString, isUndefined } from 'lodash';
|
||||||
import { notImplemented } from 'boom';
|
import { notImplemented } from 'boom';
|
||||||
import joi from 'joi';
|
import joi from 'joi';
|
||||||
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
|
const sequelizeKeys = ['include', 'order', 'limit', 'offset'];
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ const getModels = (request) => {
|
|||||||
const noRequestModels = !request.models;
|
const noRequestModels = !request.models;
|
||||||
if (noGetDb && noRequestModels) {
|
if (noGetDb && noRequestModels) {
|
||||||
return notImplemented('`request.getDb` or `request.models` are not defined.'
|
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();
|
const { models } = noGetDb ? request : request.getDb();
|
||||||
@ -17,41 +18,69 @@ const getModels = (request) => {
|
|||||||
return models;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseInclude = request => {
|
export const parseInclude = async(request) => {
|
||||||
|
if (typeof request.query.include === 'undefined') return [];
|
||||||
|
|
||||||
const include = Array.isArray(request.query.include)
|
const include = Array.isArray(request.query.include)
|
||||||
`JSON.parse` will throw if not handed valid JSON. We should use a try/catch here.
what code inside the catch do you prefer? what code inside the catch do you prefer?
https://github.com/mdibaiee/hapi-sequelize-crud/blob/5ba9d7d26100f1a1a82367097342a8c35d43a26f/src/utils.js#L50-L54 and https://github.com/mdibaiee/hapi-sequelize-crud/blob/5ba9d7d26100f1a1a82367097342a8c35d43a26f/src/utils.js#L78-L83 are both options. Thanks!
|
|||||||
? request.query.include
|
? request.query.include
|
||||||
maybe like this @joeybaker? maybe like this @joeybaker?
Kinda! The try/catch part is right, but the joi check should be a part of the validation. What you have here will always pass the What do you think of something like this in
Kinda! The try/catch part is right, but the joi check should be a part of the validation. What you have here will always pass the `if` condition, because `joi.string()` always returns a truthy value.
What do you think of something like this in `crud.js` on line 77?
``` js
const assocationJSONvaliation = joi.string().regex(/^\{.*?"model":.*?\}$/);
const associationValidation = {
include: [
joi.array().items(validAssociations),
joi.array().items(assocationJSONvaliation),
validAssociations,
assocationJSONvaliation,
],
};
```
but with this code, it won't validate the "model" name and the "where" object isn't it? but with this code, it won't validate the "**model**" name and the "**where**" object isn't it?
if the JSON string doesn't parse good so it caught as a string, but later it won't pass because crud.js that i pushed has something like `joi.string().valid(...modelAssociations)`
currently my version of crud.js has this to validate its association object:
currently my version of crud.js has this to validate its association object:
```
joi.object().keys({
model: joi.string().valid(...modelAssociations),
where: joi.object().keys({
...attributeValidation,
...sequelizeOperators,
}),
})
```
I get it. I was opting for: "if you pass JSON, we're going to skip validating it … it's too complex to do correctly". Here's the problem I see with:
It ensures you get a valid JS object to validate. But you won't. You'll get a string that contains JSON. That's why I was opting for the regex of a string. Did I get confused there? I get it. I was opting for: "if you pass JSON, we're going to skip validating it … it's too complex to do correctly".
Here's the problem I see with:
``` js
joi.object().keys({
model: joi.string().valid(...modelAssociations),
where: joi.object().keys({
...attributeValidation,
...sequelizeOperators,
}),
})
```
It ensures you get a valid JS object to validate. But you won't. You'll get a string that contains JSON. That's why I was opting for the regex of a string. Did I get confused there?
sorry @joeybaker, but i still didn't get it, what the main purpose of adding the json validation you want? because i have tried several cases of using include parameter and i didn't find any include parameter validation problem sorry @joeybaker, but i still didn't get it, what the main purpose of adding the json validation you want? because i have tried several cases of using include parameter and i didn't find any include parameter validation problem
|
|||||||
: [request.query.include]
|
: [request.query.include]
|
||||||
;
|
;
|
||||||
|
|
||||||
const models = getModels(request);
|
const models = getModels(request);
|
||||||
if (models.isBoom) return models;
|
if (models.isBoom) return models;
|
||||||
|
|
||||||
return include.map(b => {
|
const getModelInstance = 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(includeItem.include);
|
||||||
|
return resolve(includeItem);
|
||||||
|
} else {
|
||||||
|
return resolve(includeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return resolve(includeItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonValidation = joi.string().regex(/^\{.*?"model":.*?\}$/);
|
||||||
|
const includes = include.map(async(b) => {
|
||||||
let a = b;
|
let a = b;
|
||||||
try {
|
try {
|
||||||
if (joi.string().regex(/^\{.*?"model":.*?\}$/)) {
|
if (!jsonValidation.validate(a).error) {
|
||||||
a = JSON.parse(b);
|
a = JSON.parse(b);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
if (typeof a !== 'object') {
|
|
||||||
const singluarOrPluralMatch = Object.keys(models).find((modelName) => {
|
|
||||||
const { _singular, _plural } = models[modelName];
|
|
||||||
return _singular === a || _plural === a;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (singluarOrPluralMatch) return models[singluarOrPluralMatch];
|
return getModelInstance(a);
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof a === 'string') return models[a];
|
|
||||||
|
|
||||||
if (a && typeof a.model === 'string' && a.model.length) {
|
|
||||||
a.model = models[a.model];
|
|
||||||
}
|
|
||||||
|
|
||||||
return a;
|
|
||||||
}).filter(identity);
|
}).filter(identity);
|
||||||
|
|
||||||
|
return await Promise.all(includes);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseWhere = request => {
|
export const parseWhere = request => {
|
||||||
|
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
@ -14,6 +14,10 @@ export default (sequelize, DataTypes) => {
|
|||||||
models.Player.belongsTo(models.Team, {
|
models.Player.belongsTo(models.Team, {
|
||||||
foreignKey: { name: 'teamId' },
|
foreignKey: { name: 'teamId' },
|
||||||
});
|
});
|
||||||
|
models.Player.belongsTo(models.Master, {
|
||||||
|
foreignKey: 'coachId',
|
||||||
|
as: 'Coach',
|
||||||
needed for association/relationship alias test needed for association/relationship alias test
Can you not use Teams and Players? Just so we don't get an overly-complex mock schema? Can you not use Teams and Players? Just so we don't get an overly-complex mock schema?
Ah! I see. Makes sense. Ah! I see. Makes sense.
is it ok then? is it ok then?
Yup! Yup!
|
|||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scopes: {
|
scopes: {
|
||||||
|
@ -14,15 +14,16 @@ const modelNames = [
|
|||||||
{ Singluar: 'City', singular: 'city', Plural: 'Cities', plural: 'cities' },
|
{ Singluar: 'City', singular: 'city', Plural: 'Cities', plural: 'cities' },
|
||||||
{ Singluar: 'Team', singular: 'team', Plural: 'Teams', plural: 'teams' },
|
{ Singluar: 'Team', singular: 'team', Plural: 'Teams', plural: 'teams' },
|
||||||
{ Singluar: 'Player', singular: 'player', Plural: 'Players', plural: 'players' },
|
{ Singluar: 'Player', singular: 'player', Plural: 'Players', plural: 'players' },
|
||||||
|
{ Singluar: 'Master', singular: 'master', Plural: 'Masters', plural: 'masters' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export default (test) => {
|
export default (test) => {
|
||||||
test.beforeEach('get an open port', async (t) => {
|
test.beforeEach('get an open port', async(t) => {
|
||||||
t.context.port = await getPort();
|
t.context.port = await getPort();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach('setup server', async (t) => {
|
test.beforeEach('setup server', async(t) => {
|
||||||
const sequelize = t.context.sequelize = new Sequelize({
|
const sequelize = t.context.sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
logging: false,
|
logging: false,
|
||||||
@ -46,25 +47,33 @@ export default (test) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await server.register({
|
await server.register({
|
||||||
register: require('../src/index.js'),
|
register: require('../src/index.js'),
|
||||||
options: {
|
options: {
|
||||||
name: dbName,
|
name: dbName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach('create data', async (t) => {
|
test.beforeEach('create data', async(t) => {
|
||||||
const { Player, Team, City } = t.context.sequelize.models;
|
const { Player, Master, Team, City } = t.context.sequelize.models;
|
||||||
const city1 = await City.create({ name: 'Healdsburg' });
|
const city1 = await City.create({ name: 'Healdsburg' });
|
||||||
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
|
const team1 = await Team.create({ name: 'Baseballs', cityId: city1.id });
|
||||||
const team2 = await Team.create({ name: 'Footballs', 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({
|
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 player2 = await Player.create({
|
||||||
const player3 = await Player.create({ name: 'Syrah', teamId: team2.id });
|
name: 'Pinot', teamId: team1.id, coachId: master1.id
|
||||||
t.context.instances = { city1, team1, team2, player1, player2, player3 };
|
});
|
||||||
|
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
|
// kill the server so that we can exit and don't leak memory
|
||||||
|
Why remove all these spaces?
sorry this is from my IDE settings, but i don't see any eslint error