8 Commits

9 changed files with 327 additions and 109 deletions

View File

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

View File

@ -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({

View File

@ -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({

View File

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

View File

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

View File

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

View File

@ -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: {

View File

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